一、定义
分布式事务:分布式事务指事务的操作位于不同的节点上,需要保证事务的 AICD 特性,一个系统涉及到多个业务系统,出错时需要全部回滚,一般采取两阶段提交(2PC)、补偿事务(TCC)、MQ事务消息
还可以使用补单操作,来完成任务的
图1-1 电商系统订单服务涉及到库存、积分、仓储服务的数据库,出现问题时,无法直接对它们进行回滚
二、TCC补偿分布式事务
1.TCC作用机制
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
-
Try 阶段主要是对业务系统做检测及资源预留
-
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
-
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入 电商系统 要扣除资金账户系统金额,电商系统和资金账户系统都要做tcc操作
1)首先在 Try 阶段,扣除资金账户余额,记录变更的扣除金额,并更改状态为支付中
2)在 Confirm 阶段,正式扣资金账户余额,更改订单状态为已支付,将扣除金额置0
3)如果第2步执行成功,那么转账成功,如果第1步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel),资金账户余额加上变更的扣除金额,更改状态为 支付失败
2.TCC执行顺序
执行顺序:try->confirm或try-cancel
正常情况的执行顺序如下所示:
若第三方服务try出现了异常,需要进行全部回滚,执行各个服务的cancel方法,异常情况执行顺序:
若是在confirm和cancel中出现了异常(机器宕机、超时等),TCC 事务框架会通过活动日志记录各个服务的状态,发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功
缺点:需要各个业务方都内嵌tcc事务框架,协调起来麻烦,复用性低
3.TCC框架:tcc-transaction框架核心代码
电商系统为服务调用者consumer
资金账户系统、红包账户系统为provider
1)电商系统下单
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = false, delayCancelExceptions = {SocketTimeoutException.class, org.apache.dubbo.remoting.TimeoutException.class})
public void makePayment(@UniqueIdentity String orderNo, 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 order = orderRepository.findByMerchantOrderNo(orderNo);
//check if the order status is DRAFT, if no, means that another call makePayment for the same order happened, ignore this call makePayment.
//更改商品状态为paying
if (order.getStatus().equals("DRAFT")) {
order.pay(redPacketPayAmount, capitalPayAmount);
try {
orderRepository.updateOrder(order);
} catch (OptimisticLockingFailureException e) {
//ignore the concurrently update order exception, ensure idempotency.
}
}
String result = capitalTradeOrderService.record(buildCapitalTradeOrderDto(order));
String result2 = redPacketTradeOrderService.record(buildRedPacketTradeOrderDto(order));
}
2)资金账户下单-Try阶段
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = DubboTransactionContextEditor.class)
@Transactional
public String record(CapitalTradeOrderDto tradeOrderDto) {
TradeOrder foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
//check if trade order has been recorded, if yes, return success directly.商品订单不存在,新增
if (foundTradeOrder == null) {
TradeOrder tradeOrder = new TradeOrder(
tradeOrderDto.getSelfUserId(),
tradeOrderDto.getOppositeUserId(),
tradeOrderDto.getMerchantOrderNo(),
tradeOrderDto.getAmount()
);
try {
//资金账户插入一笔交易商品订单
tradeOrderRepository.insert(tradeOrder);
//根据用户id查询用户对应的资金账户信息
CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
//扣减用户资金账户余额
transferFromAccount.transferFrom(tradeOrderDto.getAmount());
//更新用户资金账户余额信息,更新余额
capitalAccountRepository.save(transferFromAccount);
} catch (DataIntegrityViolationException e) {
//this exception may happen when insert trade order concurrently, if happened, ignore this insert operation.
}
}
return "success";
}
3)资金账户确认阶段-Confirm
@Transactional(rollbackFor = Exception.class)
public void confirmRecord(CapitalTradeOrderDto tradeOrderDto) {
//根据商品订单号查询一笔交易商品订单
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
//check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
//检查资金账户交易订单是否为try的DRAFT状态
if (tradeOrder != null && tradeOrder.getStatus().equals("DRAFT")) {
//更改商品订单状态为CONFIRM
tradeOrder.confirm();
tradeOrderRepository.update(tradeOrder);
//根据用户id查询用户对应的资金账户信息
CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());
//增加用户账户资金余额
//这里应该是要正式扣除,变更状态后,需要将扣除金额的字段置0,然后余额不做变化,此资金账户系统中没有变更金额字段,
// 因此最后资金又返还了,交易过程中资金流向不会出现幂等,正式场景要修改下面两步操作
transferToAccount.transferTo(tradeOrderDto.getAmount());
//更新用户资金账户余额信息,更新余额
capitalAccountRepository.save(transferToAccount);
}
}
4)资金账户取消阶段-Cancel
@Transactional(rollbackFor = Exception.class)
public void cancelRecord(CapitalTradeOrderDto tradeOrderDto) {
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
//check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
//订单状态为DRAFT,更改为CANCEL状态
if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
//更改商品订单状态为CANCEL
tradeOrder.cancel();
tradeOrderRepository.update(tradeOrder);
//根据用户id查询用户对应的资金账户信息
CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
//增加用户账户资金余额
capitalAccount.cancelTransfer(tradeOrderDto.getAmount());
//更新用户资金账户余额信息,更新余额
capitalAccountRepository.save(capitalAccount);
}
}
红包账户服务同2)、3)、4)
5)电商系统账户阶段-Confirm
public void confirmMakePayment(String orderNo, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
Order foundOrder = orderRepository.findByMerchantOrderNo(orderNo);
//check if the trade order status is PAYING, if no, means another call confirmMakePayment happened, return directly, ensure idempotency.
//Confirm阶段:更该商品状态为CONFIRMED
if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
foundOrder.confirm();
orderRepository.updateOrder(foundOrder);
}
}
6)电商系统账户阶段-Cancel
public void cancelMakePayment(String orderNo, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
Order foundOrder = orderRepository.findByMerchantOrderNo(orderNo);
//check if the trade order status is PAYING, if no, means another call cancelMakePayment happened, return directly, ensure idempotency.
//Concel阶段:更改商品状态为PAY_FAILED
if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
foundOrder.cancelPayment();
orderRepository.updateOrder(foundOrder);
}
}
4.系统搭建测试
详情请看:https://github.com/changmingxie/tcc-transaction/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%971.2.x
这里使用http调用方式,
前提条件:导入依赖、更改数据库连接配置、生成sql脚本文件、变更配置中心、war包部署
注意:提供的http接口号与配置的tomacat端口号需要不一致,否则会出现地址被占用情况,打包的时http的war包
1)启动三个服务
2)进入order电商系统主界面
3)点击商品列表购买商品
4)正常购买各个系统会打印4.2的日志信息
这里测试正常和异常情况,正常情况输入红包金额小于红包可用余额或者购买的商品价格小于可用账户余额即可,
异常情况测试红包金额大于可用红包账户系统余额的情况
5)异常情况说明
红包账户系统前两次正常购买正常,第三次出现异常,最后执行了cancel方法进行回滚操作
资金账户系统前两次购买正常,后一次由于红包系统出现了异常,try后调用cancel方法,进行回滚
电商系统账户前两次购买正常,后一次由于红包系统出现了异常,本身也会出现异常,之后执行cancel方法
可以看到,执行顺序为:
正常情况下:order try->capital try->redpacket try->order confirm->capital confirm->redpacket confirm
异常情况(redpacket):order try->capital try->redpacket try->redpacket异常->order concel->capital concel->redpacket concel