1. 程式人生 > >dubbo分散式事務

dubbo分散式事務

一、背景

       目前開發的專案是分散式架構的,資料庫也是分開的,各個子工程之間是通過dubbo呼叫,由於沒有考慮分散式事務的問題,導致接口出錯回滾時,呼叫端正常回滾了但是被呼叫端卻不能回滾,產生了很多垃圾資料。

二、分散式事務(事務補償機制)

 事務補償即在事務鏈中的任何一個正向事務操作,都必須存在一個完全符合回滾規則的可逆事務。如果是一個完整的事務鏈,則必須事務鏈中的每一個業務服務或操作都有對應的可逆服務。在上面方式中可以看到需要手工編寫大量的程式碼來處理以保證事務的完整性,我們可以考慮實現一個通用的事務管理器,實現事務鏈和事務上下文的管理。對於事務鏈上的任何一個服務正向和逆向操作均在事務管理和協同器上註冊,由事務管理器接管所有的事務補償和回滾操作。

三、tcc-transaction框架介紹

 介紹:tcc-transaction是開源的TCC補償性分散式事務框架,Git地址:https://github.com/changmingxie/tcc-transaction
 TCC-Transaction 通過 Dubbo 隱式傳參的功能,避免自己對業務程式碼的入侵。

四、例子

 

首先我們簡單瞭解下這個專案。

  • 首頁 => 商品列表 => 確認支付頁 => 支付結果頁
  • 使用賬戶餘額 + 紅包餘額聯合支付購買商品,並賬戶之間轉賬。

專案拆分三個子 Maven 專案:

  • tcc-transaction-http-order
     :商城服務,提供商品和商品訂單邏輯。
  • tcc-transaction-http-capital :資金服務,提供賬戶餘額邏輯。
  • tcc-transaction-http-redpacket :紅包服務,提供紅包餘額邏輯。

2. 實體結構

2.1 商城服務

  • Shop,商店表。實體程式碼如下:

     

    public class Shop {

    /**

    * 商店編號

    */

    private long id;

    /**

    * 所有者使用者編號

    */

    private long ownerUserId;

    }

  • Product,商品表。實體程式碼如下:

     

    public class Product implements Serializable {

    /**

    * 商品編號

    */

    private long productId;

    /**

    * 商店編號

    */

    private long shopId;

    /**

    * 商品名

    */

    private String productName;

    /**

    * 單價

    */

    private BigDecimal price;

    }

  • Order,訂單表。實現程式碼如下:

     

    public class Order implements Serializable {

    private static final long serialVersionUID = -5908730245224893590L;

    /**

    * 訂單編號

    */

    private long id;

    /**

    * 支付( 下單 )使用者編號

    */

    private long payerUserId;

    /**

    * 收款( 商店擁有者 )使用者編號

    */

    private long payeeUserId;

    /**

    * 紅包支付金額

    */

    private BigDecimal redPacketPayAmount;

    /**

    * 賬戶餘額支付金額

    */

    private BigDecimal capitalPayAmount;

    /**

    * 訂單狀態

    * - DRAFT :草稿

    * - PAYING :支付中

    * - CONFIRMED :支付成功

    * - PAY_FAILED :支付失敗

    */

    private String status = "DRAFT";

    /**

    * 商戶訂單號,使用 UUID 生成

    */

    private String merchantOrderNo;

    /**

    * 訂單明細陣列

    * 非儲存欄位

    */

    private List<OrderLine> orderLines = new ArrayList<OrderLine>();

    }

  • OrderLine,訂單明細。實體程式碼如下:

     

    public class OrderLine implements Serializable {

    private static final long serialVersionUID = 2300754647209250837L;

    /**

    * 訂單編號

    */

    private long id;

    /**

    * 商品編號

    */

    private long productId;

    /**

    * 數量

    */

    private int quantity;

    /**

    * 單價

    */

    private BigDecimal unitPrice;

    }

業務邏輯:

下單時,插入訂單狀態為 "DRAFT" 的訂單( Order )記錄,並插入購買的商品訂單明細( OrderLine )記錄。支付時,更新訂單狀態為 "PAYING"

  • 訂單支付成功,更新訂單狀態為 "CONFIRMED"
  • 訂單支付失敗,更新訂單狀體為 "PAY_FAILED"

2.2 資金服務

關係較為簡單,有兩個實體:

  • CapitalAccount,資金賬戶餘額。實體程式碼如下:

     

    public class CapitalAccount {

    /**

    * 賬戶編號

    */

    private long id;

    /**

    * 使用者編號

    */

    private long userId;

    /**

    * 餘額

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易訂單表。實體程式碼如下:

     

    public class TradeOrder {

    /**

    * 交易訂單編號

    */

    private long id;

    /**

    * 轉出使用者編號

    */

    private long selfUserId;

    /**

    * 轉入使用者編號

    */

    private long oppositeUserId;

    /**

    * 商戶訂單號

    */

    private String merchantOrderNo;

    /**

    * 金額

    */

    private BigDecimal amount;

    /**

    * 交易訂單狀態

    * - DRAFT :草稿

    * - CONFIRM :交易成功

    * - CANCEL :交易取消

    */

    private String status = "DRAFT";

    }

業務邏輯:

訂單支付支付中,插入交易訂單狀態為 "DRAFT" 的訂單( TradeOrder )記錄,並更新減少下單使用者的資金賬戶餘額。

  • 訂單支付成功,更新交易訂單狀態為 "CONFIRM",並更新增加商店擁有使用者的資金賬戶餘額。
  • 訂單支付失敗,更新交易訂單狀態為 "CANCEL",並更新增加( 恢復 )下單使用者的資金賬戶餘額。

2.3 紅包服務

關係較為簡單,和資金服務 99.99% 相同,有兩個實體:

  • RedPacketAccount,紅包賬戶餘額。實體程式碼如下:

     

    public class RedPacketAccount {

    /**

    * 賬戶編號

    */

    private long id;

    /**

    * 使用者編號

    */

    private long userId;

    /**

    * 餘額

    */

    private BigDecimal balanceAmount;

    }

  • TradeOrder,交易訂單表。實體程式碼如下:

     

    public class TradeOrder {

    /**

    * 交易訂單編號

    */

    private long id;

    /**

    * 轉出使用者編號

    */

    private long selfUserId;

    /**

    * 轉入使用者編號

    */

    private long oppositeUserId;

    /**

    * 商戶訂單號

    */

    private String merchantOrderNo;

    /**

    * 金額

    */

    private BigDecimal amount;

    /**

    * 交易訂單狀態

    * - DRAFT :草稿

    * - CONFIRM :交易成功

    * - CANCEL :交易取消

    */

    private String status = "DRAFT";

    }

業務邏輯:

訂單支付支付中,插入交易訂單狀態為 "DRAFT" 的訂單( TradeOrder )記錄,並更新減少下單使用者的紅包賬戶餘額。

  • 訂單支付成功,更新交易訂單狀態為 "CONFIRM",並更新增加商店擁有使用者的紅包賬戶餘額。
  • 訂單支付失敗,更新交易訂單狀態為 "CANCEL",並更新增加( 恢復 )下單使用者的紅包賬戶餘額。

3. 服務呼叫

服務之間,通過 HTTP 進行呼叫。

紅包服務和資金服務為商城服務提供呼叫( 以資金服務為例子 ):

  • XML 配置如下 :

     

    // appcontext-service-provider.xml

    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <bean name="capitalAccountRepository"

    class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/>

    <bean name="tradeOrderRepository"

    class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/>

    <bean name="capitalTradeOrderService"

    class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/>

    <bean name="capitalAccountService"

    class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/>

    <bean name="capitalTradeOrderServiceExporter"

    class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">

    <property name="service" ref="capitalTradeOrderService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/>

    </bean>

    <bean name="capitalAccountServiceExporter"

    class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">

    <property name="service" ref="capitalAccountService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/>

    </bean>

    <bean id="httpServer"

    class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">

    <property name="contexts">

    <util:map>

    <entry key="/remoting/CapitalTradeOrderService" value-ref="capitalTradeOrderServiceExporter"/>

    <entry key="/remoting/CapitalAccountService" value-ref="capitalAccountServiceExporter"/>

    </util:map>

    </property>

    <property name="port" value="8081"/>

    </bean>

    </beans>

  • Java 程式碼實現如下 :

     

    public class CapitalAccountServiceImpl implements CapitalAccountService {

    @Autowired

    CapitalAccountRepository capitalAccountRepository;

    @Override

    public BigDecimal getCapitalAccountByUserId(long userId) {

    return capitalAccountRepository.findByUserId(userId).getBalanceAmount();

    }

    }

    public class CapitalAccountServiceImpl implements CapitalAccountService {

    @Autowired

    CapitalAccountRepository capitalAccountRepository;

    @Override

    public BigDecimal getCapitalAccountByUserId(long userId) {

    return capitalAccountRepository.findByUserId(userId).getBalanceAmount();

    }

    }


商城服務呼叫

  • XML 配置如下:

     

    // appcontext-service-consumer.xml

    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="httpInvokerRequestExecutor"

    class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor">

    <property name="httpClient">

    <bean class="org.apache.commons.httpclient.HttpClient">

    <property name="httpConnectionManager">

    <ref bean="multiThreadHttpConnectionManager"/>

    </property>

    </bean>

    </property>

    </bean>

    <bean id="multiThreadHttpConnectionManager"

    class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager">

    <property name="params">

    <bean class="org.apache.commons.httpclient.params.HttpConnectionManagerParams">

    <property name="connectionTimeout" value="200000"/>

    <property name="maxTotalConnections" value="600"/>

    <property name="defaultMaxConnectionsPerHost" value="512"/>

    <property name="soTimeout" value="5000"/>

    </bean>

    </property>

    </bean>

    <bean id="captialTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">

    <property name="serviceUrl" value="http://localhost:8081/remoting/CapitalTradeOrderService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/>

    <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>

    </bean>

    <bean id="capitalAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">

    <property name="serviceUrl" value="http://localhost:8081/remoting/CapitalAccountService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/>

    <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>

    </bean>

    <bean id="redPacketAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">

    <property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketAccountService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService"/>

    <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>

    </bean>

    <bean id="redPacketTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">

    <property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketTradeOrderService"/>

    <property name="serviceInterface"

    value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/>

    <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>

    </bean>

    </beans>

  • Java 介面介面如下:

     

    public interface CapitalAccountService {

    BigDecimal getCapitalAccountByUserId(long userId);

    }

    public interface CapitalTradeOrderService {

    String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);

    }

    public interface RedPacketAccountService {

    BigDecimal getRedPacketAccountByUserId(long userId);

    }

    public interface RedPacketTradeOrderService {

    String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto);

    }

4. 下單支付流程

ps:資料訪問的方法,請自己拉取程式碼,使用 IDE 檢視。謝謝。?

下單支付流程,整體流程如下圖( 開啟大圖 ):

點選【支付】按鈕,下單支付流程。實現程式碼如下:

 

@Controller

@RequestMapping("")

public class OrderController {

@RequestMapping(value = "/placeorder", method = RequestMethod.POST)

public ModelAndView placeOrder(@RequestParam String redPacketPayAmount,

@RequestParam long shopId,

@RequestParam long payerUserId,

@RequestParam long productId) {

PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);

// 下單並支付訂單

String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(),

request.getProductQuantities(), request.getRedPacketPayAmount());

// 返回

ModelAndView mv = new ModelAndView("pay_success");

// 查詢訂單狀態

String status = orderService.getOrderStatusByMerchantOrderNo(merchantOrderNo);

// 支付結果提示

String payResultTip = null;

if ("CONFIRMED".equals(status)) {

payResultTip = "支付成功";

} else if ("PAY_FAILED".equals(status)) {

payResultTip = "支付失敗";

}

mv.addObject("payResult", payResultTip);

// 商品資訊

mv.addObject("product", productRepository.findById(productId));

// 資金賬戶金額 和 紅包賬戶金額

mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(payerUserId));

mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(payerUserId));

return mv;

}

}


呼叫 PlaceOrderService#placeOrder(...) 方法,下單並支付訂單。實現程式碼如下:

 

@Service

public class PlaceOrderServiceImpl {

public String placeOrder(long payerUserId, long shopId, List<Pair<Long, Integer>> productQuantities, BigDecimal redPacketPayAmount) {

// 獲取商店

Shop shop = shopRepository.findById(shopId);

// 建立訂單

Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);

// 發起支付

Boolean result = false;

try {

paymentService.makePayment(order, redPacketPayAmount, order.getTotalAmount().subtract(redPacketPayAmount));

} catch (ConfirmingException confirmingException) {

// exception throws with the tcc transaction status is CONFIRMING,

// when tcc transaction is confirming status,

// the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent.

result = true;

} catch (CancellingException cancellingException) {

// exception throws with the tcc transaction status is CANCELLING,

// when tcc transaction is under CANCELLING status,

// the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent.

} catch (Throwable e) {

// other exceptions throws at TRYING stage.

// you can retry or cancel the operation.

e.printStackTrace();

}

return order.getMerchantOrderNo();

}

}

  • 呼叫 ShopRepository#findById(...) 方法,查詢商店。
  • 呼叫 OrderService#createOrder(...) 方法,建立訂單狀態為 "DRAFT" 的商城訂單。實際業務不會這麼做,此處僅僅是例子,簡化流程。實現程式碼如下:

     

    @Service

    public class OrderServiceImpl {

    @Transactional

    public Order createOrder(long payerUserId, long payeeUserId, List<Pair<Long, Integer>> productQuantities) {

    Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities);

    orderRepository.createOrder(order);

    return order;

    }

    }

  • 呼叫 PaymentService#makePayment(...) 方法,發起支付,TCC 流程

4.1 Try 階段

商城服務

呼叫 PaymentService#makePayment(...) 方法,發起 Try 流程,實現程式碼如下:

 

@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment")

@Transactional

public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

System.out.println("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 更新訂單狀態為支付中

order.pay(redPacketPayAmount, capitalPayAmount);

orderRepository.updateOrder(order);

// 資金賬戶餘額支付訂單

String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order));

// 紅包賬戶餘額支付訂單

String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order));

}

  • 設定方法註解 @Compensable

    • 事務傳播級別 Propagation.REQUIRED ( 預設值 )
    • 設定 confirmMethod / cancelMethod 方法名
    • 事務上下文編輯類 DefaultTransactionContextEditor ( 預設值 )
  • 設定方法註解 @Transactional,保證方法操作原子性。

  • 呼叫 OrderRepository#updateOrder(...) 方法,更新訂單狀態為支付中。實現程式碼如下:

     

    // Order.java

    public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

    this.redPacketPayAmount = redPacketPayAmount;

    this.capitalPayAmount = capitalPayAmount;

    this.status = "PAYING";

    }

  • 呼叫 TradeOrderServiceProxy#record(...) 方法,資金賬戶餘額支付訂單。實現程式碼如下:

     

    // TradeOrderServiceProxy.java

    @Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)

    public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

    return capitalTradeOrderService.record(transactionContext, tradeOrderDto);

    }

    // CapitalTradeOrderService.java

    public interface CapitalTradeOrderService {

    String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);

    }

    • 設定方法註解 @Compensable

      • propagation=Propagation.SUPPORTS :支援當前事務,如果當前沒有事務,就以非事務方式執行。為什麼不使用 REQUIRED ?如果使用 REQUIRED 事務傳播級別,事務恢復重試時,會發起新的事務。
      • confirmMethodcancelMethod 使用和 try 方法相同方法名:本地發起遠端服務 TCC confirm / cancel 階段,呼叫相同方法進行事務的提交或回滾。遠端服務的 CompensableTransactionInterceptor 會根據事務的狀態是 CONFIRMING / CANCELLING 來呼叫對應方法。
    • 呼叫 CapitalTradeOrderService#record(...) 方法,遠端呼叫,發起資金賬戶餘額支付訂單。

  • 呼叫 TradeOrderServiceProxy#record(...) 方法,紅包賬戶餘額支付訂單。和資金賬戶餘額支付訂單 99.99% 類似,不重複“複製貼上”。


資金服務

呼叫 CapitalTradeOrderServiceImpl#record(...) 方法,紅包賬戶餘額支付訂單。實現程式碼如下:

 

@Override

@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)

@Transactional

public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

// 除錯用

try {

Thread.sleep(1000l);

// Thread.sleep(10000000L);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("capital try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 生成交易訂單

TradeOrder tradeOrder = new TradeOrder(

tradeOrderDto.getSelfUserId(),

tradeOrderDto.getOppositeUserId(),

tradeOrderDto.getMerchantOrderNo(),

tradeOrderDto.getAmount()

);

tradeOrderRepository.insert(tradeOrder);

// 更新減少下單使用者的資金賬戶餘額

CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

transferFromAccount.transferFrom(tradeOrderDto.getAmount());

capitalAccountRepository.save(transferFromAccount);

return "success";

}

  • 設定方法註解 @Compensable

    • 事務傳播級別 Propagation.REQUIRED ( 預設值 )
    • 設定 confirmMethod / cancelMethod 方法名
    • 事務上下文編輯類 DefaultTransactionContextEditor ( 預設值 )
  • 設定方法註解 @Transactional,保證方法操作原子性。

  • 呼叫 TradeOrderRepository#insert(...) 方法,生成訂單狀態為 "DRAFT" 的交易訂單。
  • 呼叫 CapitalAccountRepository#save(...) 方法,更新減少下單使用者的資金賬戶餘額。Try 階段鎖定資源時,一定要先扣。TCC 是最終事務一致性,如果先新增,可能被使用。

4.2 Confirm / Cancel 階段

當 Try 操作全部成功時,發起 Confirm 操作。
當 Try 操作存在任務失敗時,發起 Cancel 操作。

4.2.1 Confirm

商城服務

呼叫 PaymentServiceImpl#confirmMakePayment(...) 方法,更新訂單狀態為支付成功。實現程式碼如下:

 

public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

// 除錯用

try {

Thread.sleep(1000l);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("order confirm make payment called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 更新訂單狀態為支付成功

order.confirm();

orderRepository.updateOrder(order);

}

  • 生產程式碼該方法需要加下 @Transactional 註解,保證原子性。
  • 呼叫 OrderRepository#updateOrder(...) 方法,更新訂單狀態為支付成功。實現程式碼如下:

     

    // Order.java

    public void confirm() {

    this.status = "CONFIRMED";

    }


資金服務

呼叫 CapitalTradeOrderServiceImpl#confirmRecord(...) 方法,更新交易訂單狀態為交易成功。

 

@Transactional

public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

// 除錯用

try {

Thread.sleep(1000l);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 查詢交易記錄

TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

// 判斷交易記錄狀態。因為 `#record()` 方法,可能事務回滾,記錄不存在 / 狀態不對

if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {

// 更新訂單狀態為交易成功

tradeOrder.confirm();

tradeOrderRepository.update(tradeOrder);

// 更新增加商店擁有者使用者的資金賬戶餘額

CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());

transferToAccount.transferTo(tradeOrderDto.getAmount());

capitalAccountRepository.save(transferToAccount);

}

}

  • 設定方法註解 @Transactional,保證方法操作原子性。
  • 判斷交易記錄狀態。因為 #record() 方法,可能事務回滾,記錄不存在 / 狀態不對。
  • 呼叫 TradeOrderRepository#update(...) 方法,更新交易訂單狀態為交易成功。
  • 呼叫 CapitalAccountRepository#save(...) 方法,更新增加商店擁有者使用者的資金賬戶餘額。實現程式碼如下:

     

    // CapitalAccount.java

    public void transferTo(BigDecimal amount) {

    this.balanceAmount = this.balanceAmount.add(amount);

    }


紅包服務

和資源服務 99.99% 相同,不重複“複製貼上”。

4.2.2 Cancel

商城服務

呼叫 PaymentServiceImpl#cancelMakePayment(...) 方法,更新訂單狀態為支付失敗。實現程式碼如下:

 

public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

// 除錯用

try {

Thread.sleep(1000l);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("order cancel make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 更新訂單狀態為支付失敗

order.cancelPayment();

orderRepository.updateOrder(order);

}

  • 生產程式碼該方法需要加下 @Transactional 註解,保證原子性。
  • 呼叫 OrderRepository#updateOrder(...) 方法,更新訂單狀態為支付失敗。實現程式碼如下:

     

    // Order.java

    public void cancelPayment() {

    this.status = "PAY_FAILED";

    }


資金服務

呼叫 CapitalTradeOrderServiceImpl#cancelRecord(...) 方法,更新交易訂單狀態為交易失敗。

 

@Transactional

public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

// 除錯用

try {

Thread.sleep(1000l);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

// 查詢交易記錄

TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

// 判斷交易記錄狀態。因為 `#record()` 方法,可能事務回滾,記錄不存在 / 狀態不對

if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {

// / 更新訂單狀態為交易失敗

tradeOrder.cancel();

tradeOrderRepository.update(tradeOrder);

// 更新增加( 恢復 )下單使用者的資金賬戶餘額

CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

capitalAccount.cancelTransfer(tradeOrderDto.getAmount());

capitalAccountRepository.save(capitalAccount);

}

}