分布式事务中间件 TCC-Transaction 源码分析 —— 项目实战
摘要: 原创出处 http://www.iocoder.cn/TCC-Transaction/http-sample/「芋道源码」欢迎转载,保留摘要,谢谢!
本文主要基于 TCC-Transaction 1.2.3.3 正式版
关注 微信公众号:【芋道源码】有福利:
- RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
- RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
- 您对于源码的疑问每条留言 都将得到 认真回复。 甚至不知道如何读源码也可以请教噢。
- 新的源码解析文章 实时收到通知。 每周更新一篇左右。
- 认真的源码交流微信群。
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,商店表。实体代码如下:
publicclassShop{/*** 商店编号*/privatelongid;/*** 所有者用户编号*/privatelongownerUserId;} -
Product,商品表。实体代码如下:
publicclassProductimplementsSerializable{/*** 商品编号*/privatelongproductId;/*** 商店编号*/privatelongshopId;/*** 商品名*/privateString productName;/*** 单价*/privateBigDecimal price;} -
Order,订单表。实现代码如下:
publicclassOrderimplementsSerializable{privatestaticfinallongserialVersionUID = -5908730245224893590L;/*** 订单编号*/privatelongid;/*** 支付( 下单 )用户编号*/privatelongpayerUserId;/*** 收款( 商店拥有者 )用户编号*/privatelongpayeeUserId;/*** 红包支付金额*/privateBigDecimal redPacketPayAmount;/*** 账户余额支付金额*/privateBigDecimal capitalPayAmount;/*** 订单状态* - DRAFT :草稿* - PAYING :支付中* - CONFIRMED :支付成功* - PAY_FAILED :支付失败*/privateString status ="DRAFT";/*** 商户订单号,使用 UUID 生成*/privateString merchantOrderNo;/*** 订单明细数组* 非存储字段*/privateList<OrderLine> orderLines =newArrayList<OrderLine>();}
-
OrderLine,订单明细。实体代码如下:
publicclassOrderLineimplementsSerializable{privatestaticfinallongserialVersionUID =2300754647209250837L;/*** 订单编号*/privatelongid;/*** 商品编号*/privatelongproductId;/*** 数量*/privateintquantity;/*** 单价*/privateBigDecimal unitPrice;}
业务逻辑:
下单时,插入订单状态为 "DRAFT"
的订单( Order )记录,并插入购买的商品订单明细( OrderLine )记录。支付时,更新订单状态为 "PAYING"
。
- 订单支付成功,更新订单状态为
"CONFIRMED"
。 - 订单支付失败,更新订单状体为
"PAY_FAILED"
。
2.2 资金服务
关系较为简单,有两个实体:
-
CapitalAccount,资金账户余额。实体代码如下:
publicclassCapitalAccount{/*** 账户编号*/privatelongid;/*** 用户编号*/privatelonguserId;/*** 余额*/privateBigDecimal balanceAmount;} -
TradeOrder,交易订单表。实体代码如下:
publicclassTradeOrder{/*** 交易订单编号*/privatelongid;/*** 转出用户编号*/privatelongselfUserId;/*** 转入用户编号*/privatelongoppositeUserId;/*** 商户订单号*/privateString merchantOrderNo;/*** 金额*/privateBigDecimal amount;/*** 交易订单状态* - DRAFT :草稿* - CONFIRM :交易成功* - CANCEL :交易取消*/privateString status ="DRAFT";}
业务逻辑:
订单支付支付中,插入交易订单状态为 "DRAFT"
的订单( TradeOrder )记录,并更新 减少下单用户的资金账户余额。
- 订单支付成功,更新交易订单状态为
"CONFIRM"
,并更新 增加商店拥有用户的资金账户余额。 - 订单支付失败,更新交易订单状态为
"CANCEL"
,并更新 增加( 恢复 )下单用户的资金账户余额。
2.3 红包服务
关系较为简单, 和资金服务 99.99% 相同,有两个实体:
-
RedPacketAccount,红包账户余额。实体代码如下:
publicclassRedPacketAccount{/*** 账户编号*/privatelongid;/*** 用户编号*/privatelonguserId;/*** 余额*/privateBigDecimal balanceAmount;} -
TradeOrder,交易订单表。实体代码如下:
publicclassTradeOrder{/*** 交易订单编号*/privatelongid;/*** 转出用户编号*/privatelongselfUserId;/*** 转入用户编号*/privatelongoppositeUserId;/*** 商户订单号*/privateString merchantOrderNo;/*** 金额*/privateBigDecimal amount;/*** 交易订单状态* - DRAFT :草稿* - CONFIRM :交易成功* - CANCEL :交易取消*/privateString status ="DRAFT";}
业务逻辑:
订单支付支付中,插入交易订单状态为 "DRAFT"
的订单( TradeOrder )记录,并更新 减少下单用户的红包账户余额。
- 订单支付成功,更新交易订单状态为
"CONFIRM"
,并更新 增加商店拥有用户的红包账户余额。 - 订单支付失败,更新交易订单状态为
"CANCEL"
,并更新 增加( 恢复 )下单用户的红包账户余额。
3. 服务调用
服务之间,通过 HTTP进行调用。
红包服务和资金服务为商城服务提供调用( 以资金服务为例子 ):
-
XML 配置如下 :
// appcontext-service-provider.xml<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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"><beanname="capitalAccountRepository"class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/><beanname="tradeOrderRepository"class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/><beanname="capitalTradeOrderService"class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/><beanname="capitalAccountService"class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/><beanname="capitalTradeOrderServiceExporter"class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter"><propertyname="service"ref="capitalTradeOrderService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/></bean><beanname="capitalAccountServiceExporter"class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter"><propertyname="service"ref="capitalAccountService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/></bean><beanid="httpServer"class="org.springframework.remoting.support.SimpleHttpServerFactoryBean"><propertyname="contexts"><util:map><entrykey="/remoting/CapitalTradeOrderService"value-ref="capitalTradeOrderServiceExporter"/><entrykey="/remoting/CapitalAccountService"value-ref="capitalAccountServiceExporter"/></util:map></property><propertyname="port"value="8081"/></bean></beans> -
Java 代码实现如下 :
publicclassCapitalAccountServiceImplimplementsCapitalAccountService{@AutowiredCapitalAccountRepository capitalAccountRepository;@OverridepublicBigDecimalgetCapitalAccountByUserId(longuserId){returncapitalAccountRepository.findByUserId(userId).getBalanceAmount();}}publicclassCapitalAccountServiceImplimplementsCapitalAccountService{@AutowiredCapitalAccountRepository capitalAccountRepository;@OverridepublicBigDecimalgetCapitalAccountByUserId(longuserId){returncapitalAccountRepository.findByUserId(userId).getBalanceAmount();}}
商城服务调用
-
XML 配置如下:
// appcontext-service-consumer.xml<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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"><beanid="httpInvokerRequestExecutor"class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"><propertyname="httpClient"><beanclass="org.apache.commons.httpclient.HttpClient"><propertyname="httpConnectionManager"><refbean="multiThreadHttpConnectionManager"/></property></bean></property></bean><beanid="multiThreadHttpConnectionManager"class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"><propertyname="params"><beanclass="org.apache.commons.httpclient.params.HttpConnectionManagerParams"><propertyname="connectionTimeout"value="200000"/><propertyname="maxTotalConnections"value="600"/><propertyname="defaultMaxConnectionsPerHost"value="512"/><propertyname="soTimeout"value="5000"/></bean></property></bean><beanid="captialTradeOrderService"class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"><propertyname="serviceUrl"value="http://localhost:8081/remoting/CapitalTradeOrderService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/><propertyname="httpInvokerRequestExecutor"ref="httpInvokerRequestExecutor"/></bean><beanid="capitalAccountService"class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"><propertyname="serviceUrl"value="http://localhost:8081/remoting/CapitalAccountService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/><propertyname="httpInvokerRequestExecutor"ref="httpInvokerRequestExecutor"/></bean><beanid="redPacketAccountService"class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"><propertyname="serviceUrl"value="http://localhost:8082/remoting/RedPacketAccountService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService"/><propertyname="httpInvokerRequestExecutor"ref="httpInvokerRequestExecutor"/></bean><beanid="redPacketTradeOrderService"class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"><propertyname="serviceUrl"value="http://localhost:8082/remoting/RedPacketTradeOrderService"/><propertyname="serviceInterface"value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/><propertyname="httpInvokerRequestExecutor"ref="httpInvokerRequestExecutor"/></bean></beans> -
Java 接口接口如下:
publicinterfaceCapitalAccountService{BigDecimalgetCapitalAccountByUserId(longuserId);}publicinterfaceCapitalTradeOrderService{Stringrecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);}publicinterfaceRedPacketAccountService{BigDecimalgetRedPacketAccountByUserId(longuserId);}publicinterfaceRedPacketTradeOrderService{Stringrecord(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto);}
4. 下单支付流程
ps:数据访问的方法,请自己拉取代码,使用 IDE 查看。谢谢。
下单支付流程,整体流程如下图( 打开大图):
点击 【支付】按钮,下单支付流程。实现代码如下:
|
- 调用
PlaceOrderService#placeOrder(...)
方法,下单并支付订单。 - 调用
OrderService#getOrderStatusByMerchantOrderNo(...)
方法,查询订单状态。
调用 PlaceOrderService#placeOrder(...)
方法,下单并支付订单。实现代码如下:
|
- 调用
ShopRepository#findById(...)
方法,查询商店。 -
调用
OrderService#createOrder(...)
方法,创建订单状态为"DRAFT"
的 商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:@ServicepublicclassOrderServiceImpl{@TransactionalpublicOrdercreateOrder(longpayerUserId,longpayeeUserId, List<Pair<Long, Integer>> productQuantities){Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities);orderRepository.createOrder(order);returnorder;}}
- 调用
PaymentService#makePayment(...)
方法,发起支付, TCC 流程。 - 生产代码对于异常需要进一步处理。
- 生产代码对于异常需要进一步处理。
- 生产代码对于异常需要进一步处理。
4.1 Try 阶段
商城服务
调用 PaymentService#makePayment(...)
方法,发起 Try 流程,实现代码如下:
|
-
设置方法注解 @Compensable
- 事务传播级别 Propagation.REQUIRED ( 默认值)
- 设置
confirmMethod
/cancelMethod
方法名 - 事务上下文编辑类 DefaultTransactionContextEditor ( 默认值)
-
设置方法注解 @Transactional,保证方法操作原子性。
-
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为 支付中。实现代码如下:// Order.javapublicvoidpay(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)publicStringrecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto){returncapitalTradeOrderService.record(transactionContext, tradeOrderDto);}// CapitalTradeOrderService.javapublicinterfaceCapitalTradeOrderService{Stringrecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);}-
设置方法注解 @Compensable
-
propagation=Propagation.SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行。 为什么不使用 REQUIRED?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。 -
confirmMethod
、cancelMethod
使用和 try 方法 相同方法名: 本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。
-
-
调用
CapitalTradeOrderService#record(...)
方法,远程调用,发起 资金账户余额支付订单。- 本地方法调用时,参数
transactionContext
传递null
即可,TransactionContextEditor 会设置。在 《TCC-Transaction 源码分析 —— TCC 实现》「6.3 资源协调者拦截器」有详细解析。 - 远程方法调用时,参数
transactionContext
需要传递。Dubbo 远程方法调用实际也进行了传递,传递方式较为特殊,通过隐式船舱,在 《TCC-Transaction 源码分析 —— Dubbo 支持》「3. Dubbo 事务上下文编辑器」有详细解析。
- 本地方法调用时,参数
-
-
调用
TradeOrderServiceProxy#record(...)
方法, 红包账户余额支付订单。和 资金账户余额支付订单 99.99% 类似,不重复“复制粘贴”。
资金服务
调用 CapitalTradeOrderServiceImpl#record(...)
方法, 红包账户余额支付订单。实现代码如下:
|
-
设置方法注解 @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(...)
方法,更新订单状态为支付 成功。实现代码如下:
|
- 生产代码该方法需要加下 @Transactional 注解,保证原子性。
-
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为支付成功。实现代码如下:// Order.javapublicvoidconfirm(){this.status ="CONFIRMED";}
资金服务
调用 CapitalTradeOrderServiceImpl#confirmRecord(...)
方法,更新交易订单状态为交易 成功。
|
- 设置方法注解 @Transactional,保证方法操作原子性。
- 判断交易记录状态。因为
#record()
方法,可能事务回滚,记录不存在 / 状态不对。 - 调用
TradeOrderRepository#update(...)
方法,更新交易订单状态为交易 成功。 -
调用
CapitalAccountRepository#save(...)
方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:// CapitalAccount.javapublicvoidtransferTo(BigDecimal amount){this.balanceAmount =this.balanceAmount.add(amount);}
红包服务
和 资源服务99.99% 相同,不重复“复制粘贴”。
4.2.2 Cancel
商城服务
调用 PaymentServiceImpl#cancelMakePayment(...)
方法,更新订单状态为支付 失败。实现代码如下:
|
- 生产代码该方法需要加下 @Transactional 注解,保证原子性。
-
调用
OrderRepository#updateOrder(...)
方法,更新订单状态为支付失败。实现代码如下:// Order.javapublicvoidcancelPayment(){this.status ="PAY_FAILED";}
资金服务
调用 CapitalTradeOrderServiceImpl#cancelRecord(...)
方法,更新交易订单状态为交易 失败。
|
- 设置方法注解 @Transactional,保证方法操作原子性。
- 判断交易记录状态。因为
#record()
方法,可能事务回滚,记录不存在 / 状态不对。 - 调用
TradeOrderRepository#update(...)
方法,更新交易订单状态为交易 失败。 -
调用
CapitalAccountRepository#save(...)
方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:// CapitalAccount.javapublicvoidcancelTransfer(BigDecimal amount){transferTo(amount);}
红包服务
和 资源服务99.99% 相同,不重复“复制粘贴”。
666. 彩蛋
嘿嘿,代码只是看起来比较多,实际不多。
蚂蚁金融云提供了银行间转账的 TCC 过程例子,有兴趣的同学可以看看: 《蚂蚁金融云 —— 分布式事务服务(DTS) —— 场景介绍》。
本系列 EOF ~撒花
胖友,分享个朋友圈,可好?!