项目搭建见:  分布式事务( 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 方法中获取

 

posted on 2022-08-01 18:19  lshan  阅读(184)  评论(0编辑  收藏  举报