TCC-Transaction 分布式事務(wù) —— 項(xiàng)目實(shí)戰(zhàn)

本文主要基于 TCC-Transaction 1.2.3.3 正式版

  1. 概述
    本文分享 TCC 項(xiàng)目實(shí)戰(zhàn)。以官方 Maven項(xiàng)目 tcc-transaction-http-sample 為例子( tcc-transaction-dubbo-sample 類(lèi)似 )基公。

首先我們簡(jiǎn)單了解下這個(gè)項(xiàng)目。

image.png

首頁(yè) => 商品列表 => 確認(rèn)支付頁(yè) => 支付結(jié)果頁(yè)
使用賬戶(hù)余額 + 紅包余額聯(lián)合支付購(gòu)買(mǎi)商品瞳脓,并賬戶(hù)之間轉(zhuǎn)賬。
項(xiàng)目拆分三個(gè)子 Maven 項(xiàng)目:

tcc-transaction-http-order :商城服務(wù),提供商品和商品訂單邏輯。
tcc-transaction-http-capital :資金服務(wù)透典,提供賬戶(hù)余額邏輯。
tcc-transaction-http-redpacket :紅包服務(wù)顿苇,提供紅包余額邏輯峭咒。

2. 實(shí)體結(jié)構(gòu)

2.1 商城服務(wù)

image.png

Shop,商店表纪岁。實(shí)體代碼如下:


public class Shop {

    /**
     * 商店編號(hào)
     */
    private long id;
    /**
     * 所有者用戶(hù)編號(hào)
     */
    private long ownerUserId;
}

Product凑队,商品表。實(shí)體代碼如下:

public class Product implements Serializable {

    /**
     * 商品編號(hào)
     */
    private long productId;
    /**
     * 商店編號(hào)
     */
    private long shopId;
    /**
     * 商品名
     */
    private String productName;
    /**
     * 單價(jià)
     */
    private BigDecimal price;
}

Order幔翰,訂單表漩氨。實(shí)現(xiàn)代碼如下:

public class Order implements Serializable {

    private static final long serialVersionUID = -5908730245224893590L;

    /**
     * 訂單編號(hào)
     */
    private long id;
    /**
     * 支付( 下單 )用戶(hù)編號(hào)
     */
    private long payerUserId;
    /**
     * 收款( 商店擁有者 )用戶(hù)編號(hào)
     */
    private long payeeUserId;
    /**
     * 紅包支付金額
     */
    private BigDecimal redPacketPayAmount;
    /**
     * 賬戶(hù)余額支付金額
     */
    private BigDecimal capitalPayAmount;
    /**
     * 訂單狀態(tài)
     * - DRAFT :草稿
     * - PAYING :支付中
     * - CONFIRMED :支付成功
     * - PAY_FAILED :支付失敗
     */
    private String status = "DRAFT";
    /**
     * 商戶(hù)訂單號(hào),使用 UUID 生成
     */
    private String merchantOrderNo;

    /**
     * 訂單明細(xì)數(shù)組
     * 非存儲(chǔ)字段
     */
    private List<OrderLine> orderLines = new ArrayList<OrderLine>();
}

OrderLine遗增,訂單明細(xì)叫惊。實(shí)體代碼如下:

public class OrderLine implements Serializable {

    private static final long serialVersionUID = 2300754647209250837L;

    /**
     * 訂單編號(hào)
     */
    private long id;
    /**
     * 商品編號(hào)
     */
    private long productId;
    /**
     * 數(shù)量
     */
    private int quantity;
    /**
     * 單價(jià)
     */
    private BigDecimal unitPrice;
}

業(yè)務(wù)邏輯:

下單時(shí),插入訂單狀態(tài)為 “DRAFT” 的訂單( Order )記錄做修,并插入購(gòu)買(mǎi)的商品訂單明細(xì)( OrderLine )記錄霍狰。支付時(shí)抡草,更新訂單狀態(tài)為 “PAYING”。

訂單支付成功蚓耽,更新訂單狀態(tài)為 “CONFIRMED”渠牲。
訂單支付失敗,更新訂單狀體為 “PAY_FAILED”步悠。
2.2 資金服務(wù)
關(guān)系較為簡(jiǎn)單签杈,有兩個(gè)實(shí)體:
CapitalAccount,資金賬戶(hù)余額鼎兽。實(shí)體代碼如下:

public class CapitalAccount {

    /**
     * 賬戶(hù)編號(hào)
     */
    private long id;
    /**
     * 用戶(hù)編號(hào)
     */
    private long userId;
    /**
     * 余額
     */
    private BigDecimal balanceAmount;
}

TradeOrder答姥,交易訂單表。實(shí)體代碼如下:

public class TradeOrder {
    /**
     * 交易訂單編號(hào)
     */
    private long id;
    /**
     * 轉(zhuǎn)出用戶(hù)編號(hào)
     */
    private long selfUserId;
    /**
     * 轉(zhuǎn)入用戶(hù)編號(hào)
     */
    private long oppositeUserId;
    /**
     * 商戶(hù)訂單號(hào)
     */
    private String merchantOrderNo;
    /**
     * 金額
     */
    private BigDecimal amount;
    /**
     * 交易訂單狀態(tài)
     * - DRAFT :草稿
     * - CONFIRM :交易成功
     * - CANCEL :交易取消
     */
    private String status = "DRAFT";
}

業(yè)務(wù)邏輯:

訂單支付支付中谚咬,插入交易訂單狀態(tài)為 “DRAFT” 的訂單( TradeOrder )記錄鹦付,并更新減少下單用戶(hù)的資金賬戶(hù)余額。

訂單支付成功择卦,更新交易訂單狀態(tài)為 “CONFIRM”敲长,并更新增加商店擁有用戶(hù)的資金賬戶(hù)余額。
訂單支付失敗秉继,更新交易訂單狀態(tài)為 “CANCEL”祈噪,并更新增加( 恢復(fù) )下單用戶(hù)的資金賬戶(hù)余額。
2.3 紅包服務(wù)
關(guān)系較為簡(jiǎn)單尚辑,和資金服務(wù) 99.99% 相同辑鲤,有兩個(gè)實(shí)體:

RedPacketAccount,紅包賬戶(hù)余額杠茬。實(shí)體代碼如下:

public class RedPacketAccount {

    /**
     * 賬戶(hù)編號(hào)
     */
    private long id;
    /**
     * 用戶(hù)編號(hào)
     */
    private long userId;
    /**
     * 余額
     */
    private BigDecimal balanceAmount;
}

TradeOrder月褥,交易訂單表。實(shí)體代碼如下:

public class TradeOrder {

    /**
     * 交易訂單編號(hào)
     */
    private long id;
    /**
     * 轉(zhuǎn)出用戶(hù)編號(hào)
     */
    private long selfUserId;
    /**
     * 轉(zhuǎn)入用戶(hù)編號(hào)
     */
    private long oppositeUserId;
    /**
     * 商戶(hù)訂單號(hào)
     */
    private String merchantOrderNo;
    /**
     * 金額
     */
    private BigDecimal amount;
    /**
     * 交易訂單狀態(tài)
     * - DRAFT :草稿
     * - CONFIRM :交易成功
     * - CANCEL :交易取消
     */
    private String status = "DRAFT";
}

業(yè)務(wù)邏輯:

訂單支付支付中瓢喉,插入交易訂單狀態(tài)為 “DRAFT” 的訂單( TradeOrder )記錄宁赤,并更新減少下單用戶(hù)的紅包賬戶(hù)余額。

訂單支付成功栓票,更新交易訂單狀態(tài)為 “CONFIRM”礁击,并更新增加商店擁有用戶(hù)的紅包賬戶(hù)余額。
訂單支付失敗逗载,更新交易訂單狀態(tài)為 “CANCEL”,并更新增加( 恢復(fù) )下單用戶(hù)的紅包賬戶(hù)余額链烈。

  1. 服務(wù)調(diào)用
    服務(wù)之間厉斟,通過(guò) HTTP 進(jìn)行調(diào)用。

紅包服務(wù)和資金服務(wù)為商城服務(wù)提供調(diào)用( 以資金服務(wù)為例子 ):

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對(duì)應(yīng)的代碼如下:

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();
    }

}

商城服務(wù)調(diào)用

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:數(shù)據(jù)訪問(wèn)的方法强衡,請(qǐng)自己拉取代碼擦秽,使用 IDE 查看.

下單支付流程,整體流程如下圖


image.png

點(diǎn)擊【支付】按鈕,下單支付流程感挥。實(shí)現(xiàn)代碼如下:

@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");
        // 查詢(xún)訂單狀態(tài)
        String status = orderService.getOrderStatusByMerchantOrderNo(merchantOrderNo);
        // 支付結(jié)果提示
        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));
        // 資金賬戶(hù)金額 和 紅包賬戶(hù)金額
        mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(payerUserId));
        mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(payerUserId));
        return mv;
    }

}

調(diào)用 PlaceOrderService#placeOrder(…) 方法缩搅,下單并支付訂單。
調(diào)用 OrderService#getOrderStatusByMerchantOrderNo(…) 方法触幼,查詢(xún)訂單狀態(tài)硼瓣。
調(diào)用 PlaceOrderService#placeOrder(…) 方法,下單并支付訂單置谦。實(shí)現(xiàn)代碼如下:

@Service
public class PlaceOrderServiceImpl {

    public String placeOrder(long payerUserId, long shopId, List<Pair<Long, Integer>> productQuantities, BigDecimal redPacketPayAmount) {
        // 獲取商店
        Shop shop = shopRepository.findById(shopId);
        // 創(chuàng)建訂單
        Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);
        // 發(fā)起支付
        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();
    }

}

調(diào)用 ShopRepository#findById(…) 方法堂鲤,查詢(xún)商店。
調(diào)用 OrderService#createOrder(…) 方法媒峡,創(chuàng)建訂單狀態(tài)為 “DRAFT” 的商城訂單瘟栖。實(shí)際業(yè)務(wù)不會(huì)這么做,此處僅僅是例子谅阿,簡(jiǎn)化流程半哟。實(shí)現(xiàn)代碼如下:

@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;
    }

}

調(diào)用 PaymentService#makePayment(…) 方法,發(fā)起支付签餐,TCC 流程寓涨。
生產(chǎn)代碼對(duì)于異常需要進(jìn)一步處理。
生產(chǎn)代碼對(duì)于異常需要進(jìn)一步處理贱田。
生產(chǎn)代碼對(duì)于異常需要進(jìn)一步處理缅茉。

4.1 Try 階段

商城服務(wù)

調(diào)用 PaymentService#makePayment(…) 方法,發(fā)起 Try 流程男摧,實(shí)現(xiàn)代碼如下:

@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"));
   // 更新訂單狀態(tài)為支付中
   order.pay(redPacketPayAmount, capitalPayAmount);
   orderRepository.updateOrder(order);
   // 資金賬戶(hù)余額支付訂單
   String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order));
   // 紅包賬戶(hù)余額支付訂單
   String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order));
}

設(shè)置方法注解 @Compensable

事務(wù)傳播級(jí)別 Propagation.REQUIRED ( 默認(rèn)值 )
設(shè)置 confirmMethod / cancelMethod 方法名
事務(wù)上下文編輯類(lèi) DefaultTransactionContextEditor ( 默認(rèn)值 )
設(shè)置方法注解 @Transactional蔬墩,保證方法操作原子性。

調(diào)用 OrderRepository#updateOrder(…) 方法耗拓,更新訂單狀態(tài)為支付中拇颅。實(shí)現(xiàn)代碼如下:

// Order.java
public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   this.redPacketPayAmount = redPacketPayAmount;
   this.capitalPayAmount = capitalPayAmount;
   this.status = "PAYING";
}

調(diào)用 TradeOrderServiceProxy#record(…) 方法,資金賬戶(hù)余額支付訂單乔询。實(shí)現(xiàn)代碼如下:

// 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);
}

設(shè)置方法注解 @Compensable

propagation=Propagation.SUPPORTS :支持當(dāng)前事務(wù)樟插,如果當(dāng)前沒(méi)有事務(wù),就以非事務(wù)方式執(zhí)行竿刁。為什么不使用 REQUIRED 黄锤?如果使用 REQUIRED 事務(wù)傳播級(jí)別,事務(wù)恢復(fù)重試時(shí)食拜,會(huì)發(fā)起新的事務(wù)鸵熟。
confirmMethod、cancelMethod 使用和 try 方法相同方法名:本地發(fā)起遠(yuǎn)程服務(wù) TCC confirm / cancel 階段负甸,調(diào)用相同方法進(jìn)行事務(wù)的提交或回滾流强。遠(yuǎn)程服務(wù)的 CompensableTransactionInterceptor 會(huì)根據(jù)事務(wù)的狀態(tài)是 CONFIRMING / CANCELLING 來(lái)調(diào)用對(duì)應(yīng)方法痹届。
調(diào)用 CapitalTradeOrderService#record(…) 方法,遠(yuǎn)程調(diào)用打月,發(fā)起資金賬戶(hù)余額支付訂單队腐。

本地方法調(diào)用時(shí),參數(shù) transactionContext 傳遞 null 即可奏篙,TransactionContextEditor 會(huì)設(shè)置柴淘。
遠(yuǎn)程方法調(diào)用時(shí),參數(shù) transactionContext 需要傳遞报破。Dubbo 遠(yuǎn)程方法調(diào)用實(shí)際也進(jìn)行了傳遞悠就,傳遞方式較為特殊,通過(guò)隱式船艙充易。
調(diào)用 TradeOrderServiceProxy#record(…) 方法梗脾,紅包賬戶(hù)余額支付訂單。和資金賬戶(hù)余額支付訂單 99.99% 類(lèi)似盹靴,不重復(fù)“復(fù)制粘貼”炸茧。

資金服務(wù)

調(diào)用 CapitalTradeOrderServiceImpl#record(…) 方法,紅包賬戶(hù)余額支付訂單稿静。實(shí)現(xiàn)代碼如下:

@Override
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 調(diào)試用
   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);
   // 更新減少下單用戶(hù)的資金賬戶(hù)余額
   CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
   transferFromAccount.transferFrom(tradeOrderDto.getAmount());
   capitalAccountRepository.save(transferFromAccount);
   return "success";
}

設(shè)置方法注解 @Compensable

事務(wù)傳播級(jí)別 Propagation.REQUIRED ( 默認(rèn)值 )
設(shè)置 confirmMethod / cancelMethod 方法名
事務(wù)上下文編輯類(lèi) DefaultTransactionContextEditor ( 默認(rèn)值 )
設(shè)置方法注解 @Transactional梭冠,保證方法操作原子性。

調(diào)用 TradeOrderRepository#insert(…) 方法改备,生成訂單狀態(tài)為 “DRAFT” 的交易訂單控漠。

調(diào)用 CapitalAccountRepository#save(…) 方法,更新減少下單用戶(hù)的資金賬戶(hù)余額悬钳。Try 階段鎖定資源時(shí)盐捷,一定要先扣。TCC 是最終事務(wù)一致性默勾,如果先添加碉渡,可能被使用。

4.2 Confirm / Cancel 階段

當(dāng) Try 操作全部成功時(shí)母剥,發(fā)起 Confirm 操作滞诺。
當(dāng) Try 操作存在任務(wù)失敗時(shí),發(fā)起 Cancel 操作环疼。

4.2.1 Confirm

商城服務(wù)

調(diào)用 PaymentServiceImpl#confirmMakePayment(…) 方法习霹,更新訂單狀態(tài)為支付成功。實(shí)現(xiàn)代碼如下:

public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   // 調(diào)試用
   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"));
   // 更新訂單狀態(tài)為支付成功
   order.confirm();
   orderRepository.updateOrder(order);
}

生產(chǎn)代碼該方法需要加下 @Transactional 注解炫隶,保證原子性淋叶。
調(diào)用 OrderRepository#updateOrder(…) 方法,更新訂單狀態(tài)為支付成功等限。實(shí)現(xiàn)代碼如下:

// Order.java
public void confirm() {
   this.status = "CONFIRMED";
}

資金服務(wù)

調(diào)用 CapitalTradeOrderServiceImpl#confirmRecord(…) 方法爸吮,更新交易訂單狀態(tài)為交易成功。

@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 調(diào)試用
   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"));
   // 查詢(xún)交易記錄
   TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
   // 判斷交易記錄狀態(tài)望门。因?yàn)?`#record()` 方法形娇,可能事務(wù)回滾,記錄不存在 / 狀態(tài)不對(duì)
   if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
       // 更新訂單狀態(tài)為交易成功
       tradeOrder.confirm();
       tradeOrderRepository.update(tradeOrder);
       // 更新增加商店擁有者用戶(hù)的資金賬戶(hù)余額
       CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());
       transferToAccount.transferTo(tradeOrderDto.getAmount());
       capitalAccountRepository.save(transferToAccount);
   }
}

設(shè)置方法注解 @Transactional筹误,保證方法操作原子性桐早。
判斷交易記錄狀態(tài)。因?yàn)?#record() 方法厨剪,可能事務(wù)回滾哄酝,記錄不存在 / 狀態(tài)不對(duì)。
調(diào)用 TradeOrderRepository#update(…) 方法祷膳,更新交易訂單狀態(tài)為交易成功陶衅。
調(diào)用 CapitalAccountRepository#save(…) 方法,更新增加商店擁有者用戶(hù)的資金賬戶(hù)余額直晨。實(shí)現(xiàn)代碼如下:

// CapitalAccount.java
public void transferTo(BigDecimal amount) {
   this.balanceAmount = this.balanceAmount.add(amount);
}

紅包服務(wù)

和資源服務(wù) 99.99% 相同搀军,不重復(fù)“復(fù)制粘貼”。

4.2.2 Cancel

商城服務(wù)

調(diào)用 PaymentServiceImpl#cancelMakePayment(…) 方法勇皇,更新訂單狀態(tài)為支付失敗罩句。實(shí)現(xiàn)代碼如下:

public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   // 調(diào)試用
   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"));
   // 更新訂單狀態(tài)為支付失敗
   order.cancelPayment();
   orderRepository.updateOrder(order);
}

生產(chǎn)代碼該方法需要加下 @Transactional 注解,保證原子性敛摘。
調(diào)用 OrderRepository#updateOrder(…) 方法门烂,更新訂單狀態(tài)為支付失敗线欲。實(shí)現(xiàn)代碼如下:

// Order.java
public void cancelPayment() {
    this.status = "PAY_FAILED";
}

資金服務(wù)

調(diào)用 CapitalTradeOrderServiceImpl#cancelRecord(…) 方法甚疟,更新交易訂單狀態(tài)為交易失敗。

@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 調(diào)試用
   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"));
   // 查詢(xún)交易記錄
   TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
   // 判斷交易記錄狀態(tài)需忿。因?yàn)?`#record()` 方法拖叙,可能事務(wù)回滾氓润,記錄不存在 / 狀態(tài)不對(duì)
   if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
       // / 更新訂單狀態(tài)為交易失敗
       tradeOrder.cancel();
       tradeOrderRepository.update(tradeOrder);
       // 更新增加( 恢復(fù) )下單用戶(hù)的資金賬戶(hù)余額
       CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
       capitalAccount.cancelTransfer(tradeOrderDto.getAmount());
       capitalAccountRepository.save(capitalAccount);
   }
}

設(shè)置方法注解 @Transactional,保證方法操作原子性薯鳍。
判斷交易記錄狀態(tài)咖气。因?yàn)?#record() 方法,可能事務(wù)回滾挖滤,記錄不存在 / 狀態(tài)不對(duì)崩溪。
調(diào)用 TradeOrderRepository#update(…) 方法,更新交易訂單狀態(tài)為交易失敗斩松。
調(diào)用 CapitalAccountRepository#save(…) 方法伶唯,更新增加( 恢復(fù) )下單用戶(hù)的資金賬戶(hù)余額。實(shí)現(xiàn)代碼如下:

/ CapitalAccount.java
public void cancelTransfer(BigDecimal amount) {
    transferTo(amount);
}

紅包服務(wù)

和資源服務(wù) 99.99% 相同惧盹,不重復(fù)“復(fù)制粘貼”乳幸。
————————————————
版權(quán)聲明:本文為CSDN博主「趁你未老」的原創(chuàng)文章瞪讼,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明粹断。
原文鏈接:https://blog.csdn.net/qq_43253123/article/details/83277580

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末符欠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瓶埋,更是在濱河造成了極大的恐慌希柿,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件养筒,死亡現(xiàn)場(chǎng)離奇詭異曾撤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)晕粪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)挤悉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人兵多,你說(shuō)我怎么就攤上這事尖啡。” “怎么了剩膘?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵衅斩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我怠褐,道長(zhǎng)畏梆,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任奈懒,我火速辦了婚禮奠涌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘磷杏。我一直安慰自己溜畅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布极祸。 她就那樣靜靜地躺著慈格,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遥金。 梳的紋絲不亂的頭發(fā)上浴捆,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音稿械,去河邊找鬼选泻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的页眯。 我是一名探鬼主播梯捕,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窝撵!你這毒婦竟也來(lái)了科阎?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忿族,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蝌矛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體道批,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年入撒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了隆豹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茅逮,死狀恐怖璃赡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情献雅,我是刑警寧澤碉考,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站挺身,受9級(jí)特大地震影響侯谁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜章钾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一墙贱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贱傀,春花似錦惨撇、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至椰棘,卻和暖如春纺棺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邪狞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工祷蝌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帆卓。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓巨朦,卻偏偏與公主長(zhǎng)得像米丘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糊啡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容