TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留,就是正常的业务(可以是一条业务链),然后保留你操作的那些数据到资源冻结数据表里面;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作,通过资源冻结表里面保留的操作数据,恢复所有被操作的业务表的数据。

    流程分析

    举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30。(冻结金额是在单独的一个字段)

image-20210724182424907.png

image-20210724182457951.png
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。

  • 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30

确认可以提交,之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

image-20210724182706011.png
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元

  • 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

需要回滚,那么就要释放冻结金额,恢复可用金额:
image-20210724182424907.png

Seata的TCC模型

image-20210724182937713.png

TCC的优缺点

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

    空回滚

    当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚
    image-20210724183426891.png
    执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

    业务悬挂

    对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂
    执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

    实现TCC模式

    :::success 解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel :::

    1.创建数据表

    我们在当前业务的数据库里面创建一个数据表,用来记录冻结数据和事务状态
    1. CREATE TABLE `account_freeze_tbl` (
    2. `xid` varchar(128) NOT NULL COMMENT '事务id',
    3. `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
    4. `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
    5. `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
    6. PRIMARY KEY (`xid`) USING BTREE
    7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

    2.声明TCC

    TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明 ```java package cn.itcast.account.service;

import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

//加一个TCC注解 @LocalTCC public interface AccountTCCService {

  1. @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
  2. void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
  3. @BusinessActionContextParameter(paramName = "money")int money);
  4. boolean confirm(BusinessActionContext ctx);
  5. boolean cancel(BusinessActionContext ctx);

}

  1. :::success
  2. deduct就是try方法,名字根据业务取的,try其实就是主业务方法.<br />在try方法上加上`@TwoPhaseBusinessAction`注解上线TCC的绑定,里面参数name就是try方法的名字,commitMethod就是confirm方法的名字,rollbackMethod就是cancel方法的名字。<br />try方法上的每个参数都要加上`@BusinessActionContextParameter`注解,这样,这些参数就可以通过`BusinessActionContext.getActionContext(String key)`方法拿到。<br />confirmcancel的参数都带有`BusinessActionContext`这个类型的参数,可以在这两个方法里面获得try中的参数。
  3. :::
  4. <a name="O0iQs"></a>
  5. ## 3.编写实现类
  6. ```java
  7. package cn.itcast.account.service.impl;
  8. import cn.itcast.account.entity.AccountFreeze;
  9. import cn.itcast.account.mapper.AccountFreezeMapper;
  10. import cn.itcast.account.mapper.AccountMapper;
  11. import cn.itcast.account.service.AccountTCCService;
  12. import io.seata.core.context.RootContext;
  13. import io.seata.rm.tcc.api.BusinessActionContext;
  14. import lombok.extern.slf4j.Slf4j;
  15. import org.springframework.beans.factory.annotation.Autowired;
  16. import org.springframework.stereotype.Service;
  17. import org.springframework.transaction.annotation.Transactional;
  18. @Service
  19. @Slf4j
  20. public class AccountTCCServiceImpl implements AccountTCCService {
  21. @Autowired
  22. private AccountMapper accountMapper;
  23. @Autowired
  24. private AccountFreezeMapper freezeMapper;
  25. //try方法,需要加上@Transactional注解
  26. @Override
  27. @Transactional
  28. public void deduct(String userId, int money) {
  29. // 0.获取事务id
  30. String xid = RootContext.getXID();
  31. //1.悬挂业务处理,判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,我要决拒绝业务
  32. AccountFreeze oldFreeze = freezeMapper.selectById(xid);
  33. if(oldFreeze!=null){
  34. //cancel,不能执行try
  35. return;
  36. }
  37. // 2.扣减可用余额,余额是无符号数,小于0会抛出异常,所以这里没有检测,正常流程应该先检测
  38. accountMapper.deduct(userId, money);
  39. // 3.记录冻结金额,事务状态
  40. AccountFreeze freeze = new AccountFreeze();
  41. freeze.setUserId(userId);
  42. freeze.setFreezeMoney(money);
  43. freeze.setState(AccountFreeze.State.TRY);
  44. freeze.setXid(xid);
  45. freezeMapper.insert(freeze);
  46. }
  47. @Override
  48. public boolean confirm(BusinessActionContext ctx) {
  49. // 1.获取事务id
  50. String xid = ctx.getXid();
  51. // 2.根据id删除冻结记录
  52. int count = freezeMapper.deleteById(xid);
  53. return count == 1;
  54. }
  55. @Override
  56. public boolean cancel(BusinessActionContext ctx) {
  57. // 0.查询冻结记录
  58. String xid = ctx.getXid();
  59. AccountFreeze freeze = freezeMapper.selectById(xid);
  60. //1.空回回滚判断,判断freeze是否为null,为null证明try没有执行,需要空回滚,空回滚也不是什么都不做,需要有回滚记录
  61. //1.1.需要知道userId,在声明TCC接口的时候在try方法的参数上写了@BusinessActionContextParameter注解,就可以通过下面的方式,拿到userID
  62. String userId = ctx.getActionContext("userId").toString();
  63. if(freeze==null){
  64. freeze = new AccountFreeze();
  65. freeze.setUserId(userId);
  66. freeze.setFreezeMoney(0);
  67. freeze.setState(AccountFreeze.State.CANCEL);
  68. freeze.setXid(xid);
  69. freezeMapper.insert(freeze);
  70. return true;
  71. }
  72. //2.幂等判断
  73. if(freeze.getState()==AccountFreeze.State.CANCEL){
  74. //已经处理过一次CANCEL了,无需重复处理
  75. return true;
  76. }
  77. // 3.恢复可用余额
  78. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
  79. // 4.将冻结金额清零,状态改为CANCEL
  80. freeze.setFreezeMoney(0);
  81. freeze.setState(AccountFreeze.State.CANCEL);
  82. int count = freezeMapper.updateById(freeze);
  83. return count == 1;
  84. }
  85. }