分布式事务 TCC-Transaction 源码分析 —— 项目实战

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 分布式事务 TCC-Transaction 源码分析 —— 项目实战

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

    1. 概述
    2. 2. 实体结构
  • 2.1 商城服务

  • 2.2 资金服务

  • 2.3 红包服务

  • 4.1 Try 阶段

  • 4.2 Confirm / Cancel 阶段

友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群和【芋艿】搞基嗨皮。

1. 概述

本文分享 TCC 项目实战。以官方 Maven项目  tcc-transaction-http-sample 为例子(  tcc-transaction-dubbo-sample 类似 )。

建议你已经成功启动了该项目。如果不知道如何启动,可以先查看《TCC-Transaction 源码分析 —— 调试环境搭建》。如果再碰到问题,欢迎加微信公众号( 芋道源码 ),我会一一仔细回复。

OK,首先我们简单了解下这个项目。

  • 首页 = 商品列表 = 确认支付页 = 支付结果页
  • 使用账户余额 + 红包余额联合支付购买商品,并账户之间转账

项目拆分三个子 Maven 项目:

  • tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。
  • tcc-transaction-http-capital :资金服务,提供账户余额逻辑。
  • tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。

你行好事会因为得到赞赏而愉悦  同理,开源项目贡献者会因为 Star 而更加有动力  为 TCC-Transaction 点赞!传送门

2. 实体结构

2.1 商城服务

  • Shop,商店表。实体代码如下:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    public class Shop {
    /**
     * 商店编号
     */
    private long id;
    /**
     * 所有者用户编号
     */
    private long ownerUserId;
    }
  • Product,商品表。实体代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    public class Product implements Serializable {
    /**
     * 商品编号
     */
    private long productId;
    /**
     * 商店编号
     */
    private long shopId;
    /**
     * 商品名
     */
    private String productName;
    /**
     * 单价
     */
    private BigDecimal price;
    }
  • Order,订单表。实现代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    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>();
    }

    /**

    • 订单编号

    /
    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,订单明细。实体代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public class OrderLine implements Serializable {
    private static final long serialVersionUID = 2300754647209250837L;
     
    /**
     * 订单编号
     */
    private long id;
    /**
     * 商品编号
     */
    private long productId;
    /**
     * 数量
     */
    private int quantity;
    /**
     * 单价
     */
    private BigDecimal unitPrice;
    }

    /**

    • 订单编号

    /
    private long id;
    /
    *

    • 商品编号

    /
    private long productId;
    /
    *

    • 数量

    /
    private int quantity;
    /
    *

    • 单价

    */
    private BigDecimal unitPrice;
    }

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

    public class Product implements Serializable {
    /**

    • 商品编号

    /
    private long productId;
    /
    *

    • 商店编号

    /
    private long shopId;
    /
    *

    • 商品名

    /
    private String productName;
    /
    *

    • 单价

    */
    private BigDecimal price;
    }

    1234567891011121314151617181920
    public class OrderLine implements Serializable {private static final long serialVersionUID = 2300754647209250837L; /** * 订单编号 */private long id;/** * 商品编号 */private long productId;/** * 数量 */private int quantity;/** * 单价 */private BigDecimal unitPrice;}

    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"

  • CapitalAccount,资金账户余额。实体代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public class CapitalAccount {
    /**
     * 账户编号
     */
    private long id;
    /**
     * 用户编号
     */
    private long userId;
    /**
     * 余额
     */
    private BigDecimal balanceAmount;
    }
  • TradeOrder,交易订单表。实体代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    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";
    }
  • public class CapitalAccount {
    /**

    • 账户编号

    /
    private long id;
    /
    *

    • 用户编号

    /
    private long userId;
    /
    *

    • 余额

    */
    private BigDecimal balanceAmount;
    }

    业务逻辑

    2.3 红包服务

    关系较为简单,和资金服务 99.99% 相同,有两个实体:

  • RedPacketAccount,红包账户余额。实体代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public class RedPacketAccount {
    /**
     * 账户编号
     */
    private long id;
    /**
     * 用户编号
     */
    private long userId;
    /**
     * 余额
     */
    private BigDecimal balanceAmount;
    }
  • TradeOrder,交易订单表。实体代码如下:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    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";
    }
  • 1234567891011121314151617181920212223242526272829
    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";}

    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 )记录,并更新减少下单用户的红包账户余额。

    **红包服务和资金服务为商城服务提供调用( 以资金服务为例子 )**:

  • XML 配置如下 :
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    // appcontext-service-provider.xml
     
     
    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

    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

  • Java 代码实现如下 :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    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();
    }
    }

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

  • 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 配置如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    // appcontext-service-consumer.xml
     
     
    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

    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

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

  • // appcontext-service-consumer.xml

    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

    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(...) 方法,下单并支付订单。
    • 调用  OrderService#getOrderStatusByMerchantOrderNo(...) 方法,查询订单状态。

    调用  PlaceOrderService#placeOrder(...) 方法,下单并支付订单。实现代码如下:

    
    @Service
    public class PlaceOrderServiceImpl {
    
        public String placeOrder(long payerUserId, long shopId, ListPairLong, 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"` 的**商城**订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @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 流程**。
  • **生产代码对于异常需要进一步处理**。
  • **生产代码对于异常需要进一步处理**。
  • **生产代码对于异常需要进一步处理**。
  • 调用  OrderService#createOrder(...) 方法,创建订单状态为  "DRAFT" 的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:

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

    生产代码对于异常需要进一步处理

    调用  PaymentService#makePayment(...) 方法,发起 Try 流程,实现代码如下:

  • 设置方法注解 @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);
      }
      
      • 本地方法调用时,参数  transactionContext 传递  null 即可,TransactionContextEditor 会设置。在《TCC-Transaction 源码分析 —— TCC 实现》「6.3 资源协调者拦截器」有详细解析。
      • 远程方法调用时,参数  transactionContext 需要传递。Dubbo 远程方法调用实际也进行了传递,传递方式较为特殊,通过隐式船舱,在《TCC-Transaction 源码分析 —— Dubbo 支持》「3. Dubbo 事务上下文编辑器」有详细解析。
      • propagation=Propagation.SUPPORTS :支持当前事务,如果当前没有事务,就以非事务方式执行。为什么不使用 REQUIRED ?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。
      • confirmMethod cancelMethod 使用和 try 方法相同方法名本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。
      • 设置方法注解 @Compensable
      • 调用 `CapitalTradeOrderService#record(...)` 方法,远程调用,发起**资金**账户余额支付订单。

      调用  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";
        }
        
      • 调用  OrderRepository#updateOrder(...) 方法,更新订单状态为支付成功。实现代码如下:

        资金服务

        调用  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);
        }
        
      • 判断交易记录状态。因为  #record() 方法,可能事务回滚,记录不存在 / 状态不对。

        调用  CapitalAccountRepository#save(...) 方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:

        红包服务

        资源服务 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";
        }
        
      • 调用  OrderRepository#updateOrder(...) 方法,更新订单状态为支付失败。实现代码如下:

        资金服务

        调用  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);
           }
        }
        
      • 设置方法注解 @Transactional,保证方法操作原子性。
      • **判断交易记录状态**。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对。
      • 调用 `TradeOrderRepository#update(...)` 方法,更新交易订单状态为交易**失败**。
      • 调用 `CapitalAccountRepository#save(...)` 方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:
        // CapitalAccount.java
        public void cancelTransfer(BigDecimal amount) {
            transferTo(amount);
        }
        
      • 判断交易记录状态。因为  #record() 方法,可能事务回滚,记录不存在 / 状态不对。

        调用  CapitalAccountRepository#save(...) 方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:

        红包服务

        资源服务 99.99% 相同,不重复“复制粘贴”。

        666. 彩蛋

        嘿嘿,代码只是看起来比较多,实际不多。

        蚂蚁金融云提供了银行间转账的 TCC 过程例子,有兴趣的同学可以看看:《蚂蚁金融云 —— 分布式事务服务(DTS) —— 场景介绍》。

        本系列 EOF ~撒花

        胖友,分享个朋友圈,可好?!

         

  • 本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 分布式事务 TCC-Transaction 源码分析 —— 项目实战


     上一篇
    Dubbo 源码解析 —— 简单原理、与spring融合 Dubbo 源码解析 —— 简单原理、与spring融合
    友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群讨论技术和源码。友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群讨论技术和源码。友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈
    2021-04-05
    下一篇 
    从一次 Snowflake 异常说起 从一次 Snowflake 异常说起
    本文主要基于真实踩坑经历展开 1. 异常概述 2. 原因分析 2.1 Snowflake工作原理 2.2 问题定位 2.3 排除时钟回拨 2.4 研究workerid 2.5 疑点 原因分析 2.2 问题
    2021-04-05