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

标签: geek | 发表时间:2018-01-02 00:00 | 作者:
出处:http://itindex.net/relian

摘要: 原创出处 http://www.iocoder.cn/TCC-Transaction/http-sample/「芋道源码」欢迎转载,保留摘要,谢谢!

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


关注 微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言 将得到 认真回复。 甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章 实时收到通知。 每周更新一篇左右
  5. 认真的源码交流微信群。

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{
    @Autowired
    CapitalAccountRepository capitalAccountRepository;
    @Override
    publicBigDecimalgetCapitalAccountByUserId(longuserId){
    returncapitalAccountRepository.findByUserId(userId).getBalanceAmount();
    }
    }
    publicclassCapitalAccountServiceImplimplementsCapitalAccountService{
    @Autowired
    CapitalAccountRepository capitalAccountRepository;
    @Override
    publicBigDecimalgetCapitalAccountByUserId(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 查看。谢谢。

下单支付流程,整体流程如下图( 打开大图):

点击 【支付】按钮,下单支付流程。实现代码如下:

      
@Controller
@RequestMapping("")
publicclassOrderController{
@RequestMapping(value ="/placeorder", method = RequestMethod.POST)
publicModelAndViewplaceOrder(@RequestParam String redPacketPayAmount,
@RequestParamlongshopId,
@RequestParamlongpayerUserId,
@RequestParamlongproductId){
PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);
// 下单并支付订单
String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(),
request.getProductQuantities(), request.getRedPacketPayAmount());
// 返回
ModelAndView mv =newModelAndView("pay_success");
// 查询订单状态
String status = orderService.getOrderStatusByMerchantOrderNo(merchantOrderNo);
// 支付结果提示
String payResultTip =null;
if("CONFIRMED".equals(status)) {
payResultTip ="支付成功";
}elseif("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));
returnmv;
}
}

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

      
@Service
publicclassPlaceOrderServiceImpl{
publicStringplaceOrder(longpayerUserId,longshopId, 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();
}
returnorder.getMerchantOrderNo();
}
}
  • 调用 ShopRepository#findById(...)方法,查询商店。
  • 调用 OrderService#createOrder(...)方法,创建订单状态为 "DRAFT"商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:

            
    @Service
    publicclassOrderServiceImpl{
    @Transactional
    publicOrdercreateOrder(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(confirmMethod ="confirmMakePayment", cancelMethod ="cancelMakePayment")
@Transactional
publicvoidmakePayment(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
    publicvoidpay(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.java
    publicinterfaceCapitalTradeOrderService{
    Stringrecord(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
publicStringrecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto){
// 调试用
try{
Thread.sleep(1000l);
// Thread.sleep(10000000L);
}catch(InterruptedException e) {
thrownewRuntimeException(e);
}
System.out.println("capital try record called. time seq:"+ DateFormatUtils.format(Calendar.getInstance(),"yyyy-MM-dd HH:mm:ss"));
// 生成交易订单
TradeOrder tradeOrder =newTradeOrder(
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(...)方法,更新订单状态为支付 成功。实现代码如下:

      
publicvoidconfirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount){
// 调试用
try{
Thread.sleep(1000l);
}catch(InterruptedException e) {
thrownewRuntimeException(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
    publicvoidconfirm(){
    this.status ="CONFIRMED";
    }

资金服务

调用 CapitalTradeOrderServiceImpl#confirmRecord(...)方法,更新交易订单状态为交易 成功

      
@Transactional
publicvoidconfirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto){
// 调试用
try{
Thread.sleep(1000l);
}catch(InterruptedException e) {
thrownewRuntimeException(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
    publicvoidtransferTo(BigDecimal amount){
    this.balanceAmount =this.balanceAmount.add(amount);
    }

红包服务

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

4.2.2 Cancel

商城服务

调用 PaymentServiceImpl#cancelMakePayment(...)方法,更新订单状态为支付 失败。实现代码如下:

      
publicvoidcancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount){
// 调试用
try{
Thread.sleep(1000l);
}catch(InterruptedException e) {
thrownewRuntimeException(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
    publicvoidcancelPayment(){
    this.status ="PAY_FAILED";
    }

资金服务

调用 CapitalTradeOrderServiceImpl#cancelRecord(...)方法,更新交易订单状态为交易 失败

      
@Transactional
publicvoidcancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto){
// 调试用
try{
Thread.sleep(1000l);
}catch(InterruptedException e) {
thrownewRuntimeException(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
    publicvoidcancelTransfer(BigDecimal amount){
    transferTo(amount);
    }

红包服务

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

666. 彩蛋

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

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

本系列 EOF ~撒花

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

相关 [分布 中间件 tcc] 推荐:

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

- - IT瘾-geek
摘要: 原创出处 http://www.iocoder.cn/TCC-Transaction/http-sample/「芋道源码」欢迎转载,保留摘要,谢谢. 本文主要基于 TCC-Transaction 1.2.3.3 正式版. 4.2 Confirm / Cancel 阶段. 微信公众号:【芋道源码】有福利:.

分布式事务的典型处理方式:2PC、TCC、异步确保和最大努力型

- - 研发管理 - ITeye博客
柔性事务满足BASE理论(基本可用,最终一致). 本文主要围绕分布式事务当中的柔性事务的处理方式进行讨论. 由于支付宝整个架构是SOA架构,因此传统单机环境下数据库的ACID事务满足了分布式环境下的业务需要,以上几种事务类似就是针对分布式环境下业务需要设定的. 两阶段型:就是分布式事务两阶段提交,对应技术上的XA、JTA/JTS.

终于有人把“TCC分布式事务”实现原理讲明白了! - JaJian - 博客园

- -
之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下. 很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务. 首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同学,可以参考之前的文章:.

TCC两阶段补偿型

- - 互联网 - ITeye博客
TCC方案是可能是目前最火的一种柔性事务方案了. 关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出. 在该论文中,TCC还是以Tentative-Confirmation-Cancellation命名.

如何选择分布式事务形态(TCC,SAGA,2PC,补偿,基于消息最终一致性等等) - YOYO&# - 博客园

- -
分布式事务有多种主流形态,包括:. 基于补偿实现的分布式事务(gts/seata自动补偿的形式). 基于TCC实现的分布式事务. 基于SAGA实现的分布式事务. 基于2PC实现的分布式事务. 因为任何事情都没有银弹,只有最合适当前场景的解决方案. 这些形态的原理已经在很多文章中进行了剖析,用“分布式事务”关键字就能搜到对应的文章,本文不再赘述这些形态的原理,并将重点放在如何根据业务选择对应的分布式事务形态上.

Redis分布式中间件TwemProxy

- - 企业架构 - ITeye博客
twemproxy,也叫nutcraker. 是一个twtter开源的一个redis和memcache代理服务器. redis作为一个高效的缓存服务器,非常具有应用价值. 但是当使用比较多的时候,就希望可以通过某种方式 统一进行管理. 避免每个应用每个客户端管理连接的松散性. 搜索了不少的开源代理项目,知乎实现的python分片客户端.

MySQL分布式中间件:MyCAT

- - 标点符
随着传统的数据库技术日趋成熟、计算机网络技术的飞速发展和应用范围的扩充,数据库应用已经普遍建立于计算机网络之上. 这时集中式数据库系统表现出它的不足:. 集中式处理,势必造成性能瓶颈;. 应用程序集中在一台计算机上运行,一旦该计算机发生故障,则整个系统受到影响,可靠性不高;. 集中式处理引起系统的规模和配置都不够灵活,系统的可扩充性差.

淘宝开源分布式消息中间件Metamorphosis

- - InfoQ cn
最近,淘宝开源了分布式消息中间件 Memorphosis项目,它是Linkedin开源MQ——Kafka的Java版本,针对淘宝内部应用做了定制和优化. 据了解,Metamorphosis(以下简称Meta)的设计原则包括:. 分布式,生产者、服务器和消费者都可分布. Metamorphosis的总体 架构图如下:.

阿里开源Mysql分布式中间件:Cobar

- - 数据库 - ITeye博客
Cobar是阿里巴巴研发的关系型数据的分布式处理系统(Amoeba的升级版,该产品成功替代了原先基于Oracle的数据存储方案,目前已经接管了3000+个MySQL数据库的schema,平均每天处理近50亿次的SQL执行请求. )(github上面的是源码,大家下来需要自己用maven2编译后运行、者放Eclipse里面运行,一开始我用maven3没有执行成功.

ShardingSphere x Seata,一致性更强的分布式数据库中间件

- - IT瘾-dev
日前,分布式数据库中间件 ShardingSphere 将Seata 分布式事务能力进行整合,旨在打造一致性更强的分布式数据库中间件. 数据库领域,分布式事务的实现主要包含:两阶段的 XA 和 BASE 柔性事务. XA 事务底层,依赖于具体的数据库厂商对 XA 两阶段提交协议的支持. 通常,XA 协议通过在 Prepare 和 Commit 阶段进行 2PL(2 阶段锁),保证了分布式事务的 ACID,适用于短事务及非云化环境(云化环境下一次 IO 操作大概需要 20ms,两阶段锁会锁住资源长达 40ms,因此热点行上的事务的 TPS 会降到 25/s 左右,非云化环境通常一次 IO 只需几毫秒,因此锁热点数据的时间相对较低).