一、背景
目前开发的项目是分布式架构的,数据库也是分开的,各个子工程之间是通过dubbo调用,由于没有考虑分布式事务的问题,导致接口出错回滚时,调用端正常回滚了但是被调用端却不能回滚,产生了很多垃圾数据。
二、分布式事务(事务补偿机制)
事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。在上面方式中可以看到需要手工编写大量的代码来处理以保证事务的完整性,我们可以考虑实现一个通用的事务管理器,实现事务链和事务上下文的管理。对于事务链上的任何一个服务正向和逆向操作均在事务管理和协同器上注册,由事务管理器接管所有的事务补偿和回滚操作。
三、tcc-transaction框架介绍
介绍:tcc-transaction是开源的TCC补偿性分布式事务框架,Git地址:https://github.com/changmingxie/tcc-transaction
TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。
四、例子
首先我们简单了解下这个项目。
项目拆分三个子 Maven 项目:
tcc-transaction-http-order
:商城服务,提供商品和商品订单逻辑。tcc-transaction-http-capital
:资金服务,提供账户余额逻辑。tcc-transaction-http-redpacket
:红包服务,提供红包余额逻辑。Shop,商店表。实体代码如下:
public class Shop { /** * 商店编号 */ private long id; /** * 所有者用户编号 */ private long ownerUserId; } |
Product,商品表。实体代码如下:
public class Product implements Serializable { /** * 商品编号 */ private long productId; /** * 商店编号 */ private long shopId; /** * 商品名 */ private String productName; /** * 单价 */ private BigDecimal price; } |
Order,订单表。实现代码如下:
public class Order implements Serializable { private static final long serialVersionUID = -5908730245224893590L; /** * 订单编号 */ private long id; /** * 支付( 下单 )用户编号 */ private long payerUserId; /** * 收款( 商店拥有者 )用户编号 */ private long payeeUserId; /** * 红包支付金额 */ private BigDecimal redPacketPayAmount; /** * 账户余额支付金额 */ private BigDecimal capitalPayAmount; /** * 订单状态 * - DRAFT :草稿 * - PAYING :支付中 * - CONFIRMED :支付成功 * - PAY_FAILED :支付失败 */ private String status = "DRAFT"; /** * 商户订单号,使用 UUID 生成 */ private String merchantOrderNo; /** * 订单明细数组 * 非存储字段 */ private List } |
OrderLine,订单明细。实体代码如下:
public class OrderLine implements Serializable { private static final long serialVersionUID = 2300754647209250837L; /** * 订单编号 */ private long id; /** * 商品编号 */ private long productId; /** * 数量 */ private int quantity; /** * 单价 */ private BigDecimal unitPrice; } |
业务逻辑:
下单时,插入订单状态为 "DRAFT"
的订单( Order )记录,并插入购买的商品订单明细( OrderLine )记录。支付时,更新订单状态为 "PAYING"
。
"CONFIRMED"
。"PAY_FAILED"
。关系较为简单,有两个实体:
CapitalAccount,资金账户余额。实体代码如下:
public class CapitalAccount { /** * 账户编号 */ private long id; /** * 用户编号 */ private long userId; /** * 余额 */ private BigDecimal balanceAmount; } |
TradeOrder,交易订单表。实体代码如下:
public class TradeOrder { /** * 交易订单编号 */ private long id; /** * 转出用户编号 */ private long selfUserId; /** * 转入用户编号 */ private long oppositeUserId; /** * 商户订单号 */ private String merchantOrderNo; /** * 金额 */ private BigDecimal amount; /** * 交易订单状态 * - DRAFT :草稿 * - CONFIRM :交易成功 * - CANCEL :交易取消 */ private String status = "DRAFT"; } |
业务逻辑:
订单支付支付中,插入交易订单状态为 "DRAFT"
的订单( TradeOrder )记录,并更新减少下单用户的资金账户余额。
"CONFIRM"
,并更新增加商店拥有用户的资金账户余额。"CANCEL"
,并更新增加( 恢复 )下单用户的资金账户余额。关系较为简单,和资金服务 99.99% 相同,有两个实体:
RedPacketAccount,红包账户余额。实体代码如下:
public class RedPacketAccount { /** * 账户编号 */ private long id; /** * 用户编号 */ private long userId; /** * 余额 */ private BigDecimal balanceAmount; } |
TradeOrder,交易订单表。实体代码如下:
public class TradeOrder { /** * 交易订单编号 */ private long id; /** * 转出用户编号 */ private long selfUserId; /** * 转入用户编号 */ private long oppositeUserId; /** * 商户订单号 */ private String merchantOrderNo; /** * 金额 */ private BigDecimal amount; /** * 交易订单状态 * - DRAFT :草稿 * - CONFIRM :交易成功 * - CANCEL :交易取消 */ private String status = "DRAFT"; } |
业务逻辑:
订单支付支付中,插入交易订单状态为 "DRAFT"
的订单( TradeOrder )记录,并更新减少下单用户的红包账户余额。
"CONFIRM"
,并更新增加商店拥有用户的红包账户余额。"CANCEL"
,并更新增加( 恢复 )下单用户的红包账户余额。服务之间,通过 HTTP 进行调用。
红包服务和资金服务为商城服务提供调用( 以资金服务为例子 ):
XML 配置如下 :
// appcontext-service-provider.xml 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"> class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/> class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/> class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/> class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/> class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter"> value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/> class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter"> value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/> class="org.springframework.remoting.support.SimpleHttpServerFactoryBean"> |
Java 代码实现如下 :
public class CapitalAccountServiceImpl implements CapitalAccountService { @Autowired CapitalAccountRepository capitalAccountRepository; @Override public BigDecimal getCapitalAccountByUserId(long userId) { return capitalAccountRepository.findByUserId(userId).getBalanceAmount(); } } public class CapitalAccountServiceImpl implements CapitalAccountService { @Autowired CapitalAccountRepository capitalAccountRepository; @Override public BigDecimal getCapitalAccountByUserId(long userId) { return capitalAccountRepository.findByUserId(userId).getBalanceAmount(); } } |
商城服务调用
XML 配置如下:
// appcontext-service-consumer.xml 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"> class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"> class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"> value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/> value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/> value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService"/> value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/> |
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); } |
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, List // 获取商店 Shop shop = shopRepository.findById(shopId); // 创建订单 Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities); // 发起支付 Boolean result = false; try { paymentService.makePayment(order, redPacketPayAmount, order.getTotalAmount().subtract(redPacketPayAmount)); } catch (ConfirmingException confirmingException) { // exception throws with the tcc transaction status is CONFIRMING, // when tcc transaction is confirming status, // the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent. result = true; } catch (CancellingException cancellingException) { // exception throws with the tcc transaction status is CANCELLING, // when tcc transaction is under CANCELLING status, // the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent. } catch (Throwable e) { // other exceptions throws at TRYING stage. // you can retry or cancel the operation. e.printStackTrace(); } return order.getMerchantOrderNo(); } } |
ShopRepository#findById(...)
方法,查询商店。调用 OrderService#createOrder(...)
方法,创建订单状态为 "DRAFT"
的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:
@Service public class OrderServiceImpl { @Transactional public Order createOrder(long payerUserId, long payeeUserId, List Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities); orderRepository.createOrder(order); return order; } } |
PaymentService#makePayment(...)
方法,发起支付,TCC 流程商城服务
调用 PaymentService#makePayment(...)
方法,发起 Try 流程,实现代码如下:
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment") @Transactional public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) { System.out.println("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss")); // 更新订单状态为支付中 order.pay(redPacketPayAmount, capitalPayAmount); orderRepository.updateOrder(order); // 资金账户余额支付订单 String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order)); // 红包账户余额支付订单 String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order)); } |
设置方法注解 @Compensable
confirmMethod
/ cancelMethod
方法名设置方法注解 @Transactional,保证方法操作原子性。
调用 OrderRepository#updateOrder(...)
方法,更新订单状态为支付中。实现代码如下:
// Order.java public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) { this.redPacketPayAmount = redPacketPayAmount; this.capitalPayAmount = capitalPayAmount; this.status = "PAYING"; } |
调用 TradeOrderServiceProxy#record(...)
方法,资金账户余额支付订单。实现代码如下:
// TradeOrderServiceProxy.java @Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class) public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) { return capitalTradeOrderService.record(transactionContext, tradeOrderDto); } // CapitalTradeOrderService.java public interface CapitalTradeOrderService { String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto); } |
设置方法注解 @Compensable
propagation=Propagation.SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行。为什么不使用 REQUIRED ?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。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(...)
方法,红包账户余额支付订单。实现代码如下:
@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
confirmMethod
/ cancelMethod
方法名设置方法注解 @Transactional,保证方法操作原子性。
TradeOrderRepository#insert(...)
方法,生成订单状态为 "DRAFT"
的交易订单。CapitalAccountRepository#save(...)
方法,更新减少下单用户的资金账户余额。Try 阶段锁定资源时,一定要先扣。TCC 是最终事务一致性,如果先添加,可能被使用。当 Try 操作全部成功时,发起 Confirm 操作。
当 Try 操作存在任务失败时,发起 Cancel 操作。
商城服务
调用 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); } |
调用 OrderRepository#updateOrder(...)
方法,更新订单状态为支付成功。实现代码如下:
// Order.java public void confirm() { this.status = "CONFIRMED"; } |
资金服务
调用 CapitalTradeOrderServiceImpl#confirmRecord(...)
方法,更新交易订单状态为交易成功。
@Transactional public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) { // 调试用 try { Thread.sleep(1000l); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss")); // 查询交易记录 TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo()); // 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对 if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) { // 更新订单状态为交易成功 tradeOrder.confirm(); tradeOrderRepository.update(tradeOrder); // 更新增加商店拥有者用户的资金账户余额 CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId()); transferToAccount.transferTo(tradeOrderDto.getAmount()); capitalAccountRepository.save(transferToAccount); } } |
#record()
方法,可能事务回滚,记录不存在 / 状态不对。TradeOrderRepository#update(...)
方法,更新交易订单状态为交易成功。调用 CapitalAccountRepository#save(...)
方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:
// CapitalAccount.java public void transferTo(BigDecimal amount) { this.balanceAmount = this.balanceAmount.add(amount); } |
红包服务
和资源服务 99.99% 相同,不重复“复制粘贴”。
商城服务
调用 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); } |
调用 OrderRepository#updateOrder(...)
方法,更新订单状态为支付失败。实现代码如下:
// Order.java public void cancelPayment() { this.status = "PAY_FAILED"; } |
资金服务
调用 CapitalTradeOrderServiceImpl#cancelRecord(...)
方法,更新交易订单状态为交易失败。
@Transactional public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) { // 调试用 try { Thread.sleep(1000l); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss")); // 查询交易记录 TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo()); // 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对 if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) { // / 更新订单状态为交易失败 tradeOrder.cancel(); tradeOrderRepository.update(tradeOrder); // 更新增加( 恢复 )下单用户的资金账户余额 CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId()); capitalAccount.cancelTransfer(tradeOrderDto.getAmount()); capitalAccountRepository.save(capitalAccount); } } |
#record()
方法,可能事务回滚,记录不存在 / 状态不对。TradeOrderRepository#update(...)
方法,更新交易订单状态为交易失败。调用 CapitalAccountRepository#save(...)
方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:
// CapitalAccount.java public void cancelTransfer(BigDecimal amount) { transferTo(amount); } |