项目搭建见: 分布式事务( XA) -- seata eurake springboot mysql (1.4.2) https://www.cnblogs.com/lshan/p/16533280.html
官网示例代码: https://github.com/seata/seata-samples
my TCC 测试代码 : https://gitee.com/lshan523/seata-demo
适用场景:由于从业务服务是同步调用,其结果会影响到主业务服务的决策,因此通用型 TCC 分布式事务解决方案适用于执行时间确定且较短的业务,比如互联网金融企业最核心的三个服务:交易、支付、账务:
案例场景:
TCC 模型的并发事务:
案例:
支付服务二阶段
1. 先调用账务服务的 Confirm 接口,扣除买家冻结资金;增加卖家可用资金。
2.调用成功后,支付服务修改支付订单为完成状态,完成支付。
此处演示冻结操作:
1. DO:
@Data @Document("account_freeze_tbl") @NoArgsConstructor @AllArgsConstructor public class AccountFreeze { @Id private String xid; private String userId; private Integer freezeMoney; private Integer state; private Boolean active=true; public AccountFreeze(String xid, String userId, Integer freezeMoney, Integer state) { this.xid = xid; this.userId = userId; this.freezeMoney = freezeMoney; this.state = state; } public static abstract class State { public final static int TRY = 0; public final static int CONFIRM = 1; public final static int CANCEL = 2; } } @Data @Document("account_tbl") public class Account { @Id private String id; private String userId; private Integer money; private Boolean active=true; }
2. 业务实现:
@TwoPhaseBusinessAction(name = "deduct", name 保证唯一
@LocalTCC public interface AccountTccService { /** * @param userId * @param money */ @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx); }
实现:
此处通过 xid 实现幂等操作
@Service public class AccountTccServiceImpl implements AccountTccService { @Autowired private AccountFreezeService accountFreezeService; @Autowired private AccountService accountService; @Override public void deduct(String userId, int money) { String xid = RootContext.getXID(); // 查询冻结记录,如果有,就是cancel执行过,不能继续执行 AccountFreeze oldfreeze = accountFreezeService.findOne(MapUtils.of("xid",xid)); if (oldfreeze != null){ return; } // 扣除 accountService.updateByKeyInc(MapUtils.of("userId",userId),"money",-money); // 记录 AccountFreeze freeze = new AccountFreeze(xid,userId,money,AccountFreeze.State.TRY); accountFreezeService.saveOrUpdate(freeze); }
// 确认提交后,删除冻结 @Override public boolean confirm(BusinessActionContext ctx) { String xid = ctx.getXid(); long l = accountFreezeService.delById(xid); return l == 1; } @Override public boolean cancel(BusinessActionContext ctx) { String xid = ctx.getXid(); // 查询冻结记录 AccountFreeze freeze = accountFreezeService.findOne(MapUtils.of("_id",xid)); if(null == freeze){ // try没有执行,需要空回滚 freeze = new AccountFreeze(xid,ctx.getActionContext("userId").toString(),0,AccountFreeze.State.CANCEL); accountFreezeService.saveOrUpdate(freeze); return true; } // 幂等判断 if(freeze.getState() == AccountFreeze.State.CANCEL){ return true; } // 恢复金额 // accountService.refund(freeze.getUserId(), freeze.getFreezeMoney()); accountService.updateByKeyInc(MapUtils.of("userId",freeze.getUserId()),"money",+freeze.getFreezeMoney()); long count = accountFreezeService.updateByQuery(MapUtils.of("xid",freeze.getXid()), MapUtils.of("freezeMoney",0,"state",AccountFreeze.State.CANCEL)); return count == 1; }
测试:
@RestController @RequestMapping("account") public class AccountController { @Autowired private AccountService accountService; @Autowired private TccHandler tccHandler; @PutMapping("/{userId}/{money}") public String deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money,Boolean isExp){ tccHandler.deduct(userId,money,isExp); return "ok"; } @PostMapping("addAccount") public ResponseEntity<Void> add(@RequestBody Account account){ accountService.saveOrUpdate(account); return ResponseEntity.noContent().build(); } }
1.创建账户初始化money 100
curl -X POST "http://localhost:7302/account/addAccount" -H "accept: */*" -H "Content-Type: application/json" -d "{ \"id\": \"1\", \"money\": 100, \"userId\": \"1\"}"
2. 冻结10 用户1, 10元 , 查看DB , 扣减正常
curl -X PUT "http://localhost:7302/account/1/10?isExp=flase" -H "accept: */*"
3. 手动制造异常,查看是否能正常回滚
curl -X PUT "http://localhost:7302/account/1/10?isExp=true" -H "accept: */*"
说明, try 方法可以传递参数到ctx
eg:
@TwoPhaseBusinessAction(name = "TccActionTwo", commitMethod = "commit", rollbackMethod = "rollback") public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b, @BusinessActionContextParameter(paramName = "c", index = 1) List list);
然后 可以通过 actionContext 在 confirm or cancal 方法中获取