• SpringCloud Alibaba - Seata 四种分布式事务解决方案(TCC、Saga)+ 实践部署(下)


    目录

    一、Seata 分布式解决方案

    1.1、TCC 模式

    1.1.1、TCC 模式理论

    对比 TCC 和 AT 模式的一致性和隔离性

    TC 的工作模型

    1.2.2、TCC 模式优缺点

    1.2.3、TCC 模式注意事项:空回滚

    1.2.4、TCC 模式注意事项:业务悬挂

    1.2.5、实现 TCC 模式

    案例:在用户余额扣减服务中,实现 TCC 模式.

    实现案例

    a)TCC 的 try、confirm、cancel 方法都需要在接口中基于注解来声明

    b)建表

    c)对应刚刚上述所描述的实现思路,可以基本实现(未考虑空回滚 和 业务悬挂) 

    d)考虑空回滚

    e)幂等问题

    f)业务悬挂问题

    g)到此,整个业务完成.

    1.2、Saga 模式

    1.2.1、Saga 模式理论

    1.2.2、saga 模式优缺点


    一、Seata 分布式解决方案


    1.1、TCC 模式

    1.1.1、TCC 模式理论

    TCC 模式和 AT 模式很相似,第一阶段都是独立事务,执行完了直接提交,不同的是 TCC 模式不用去加锁,也不用生成快照,因此性能上会更好.

    TCC 模式的第二阶段是基于人工编码的方式来实现数据恢复的,不像 AT 是自动实现的.

    人工编码的方式需要实现三个方法,分别是 try、confirm、cancel.

    • try:用来进行资源的检测和预留. 也就是说我需要修改某个资源的时候,先把这个资源预留下来,等到第二阶段的时候在对这个资源进行一个具体的操作.
    • cancel:如果第二阶段需要进行回滚,就执行 cancel. cancel 是对预留资源的一个释放(可以理解为对 try 的反向操作).
    • confirm:完成资源业务操作(这里要求 try 成功,那么 confirm 一定要成功).

    例如现在我的账户余额是 100 元,现在要扣掉 30 元.  如果分成 try、cancel、confirm 这三个阶段.

    1. try 阶段:做资金的检测和预留.  检测就是判断余额够不够(当前余额为 100,要扣 30 肯定是够的).  预留就是说如果余额充足,就先把要扣的 30 元金额冻结起来,也就是说可用余额扣减 30,冻结金额增加 30,而总金额是没有变化的.
    2. confirm 阶段:如果 try 阶段执行成功了,就会执行 confirm 进行提交.  这里就提交就是直接把 try 阶段冻结的 30 元金额直接扣掉,因此总金额就变成 70 元了.
    3. cancel 阶段:如果有人在执行 try 阶段失败了,就要执行 cancel 进行回滚.  这里就是对 try 阶段的一个反向操作.  在 try 阶段冻结的30元余额解冻,可用余额也就增加了 30.   因此从 try 到 cancel 可以看到总金额是没有变化的,变化的仅仅是冻结的部分.
    对比 TCC 和 AT 模式的一致性和隔离性

    一致性:首先第一阶段两个模式都是各自提交各自的事务,因此两种模式都有可能出现提交成功和失败的情况,导致状态不一致,需要通过第二阶段来调整.  也就是说这两种模式都是最终一致性.

    隔离性:AT 模式是需要通过加锁实现隔离(在第一、第二阶段持有全局锁),而 TCC 模式下不需要加锁隔离,因为在第一阶段是通过冻结来实现隔离(冻结了一部分金额),就算此时有另一个事务也要冻结金额,那就直接从可用余额中取一部分冻结,所以事务之间都没有任何影响,不需要加锁,那么 TCC 模式的性能就要比 AT 模式好很多了.

    TC 的工作模型

    第一阶段:

    这里大部分都和 AT 很像,一开始都是由 TM 去开启全局事务并注册到 TC 上面,然后 TM 去通知每一个分支事务去执行,然后请求被 RM 拦截,RM 就会先去注册分之十五,然后去执行 try 预留资源,执行完后直接提交,随后向 TC 报告事务的状态(资源预留执行成功了?还是失败了).

    第二阶段:

    TM 通知 TC 事务结束了,那么 TC 就要对事务的状态做判断了.  如果分支预留资源成功了,就直接执行 confirm 提交即可;如果发现其中任意一个有问题,就要执行 cancel 逻辑.

    1.2.2、TCC 模式优缺点

    优点:

    性能高:第一阶段执行完直接提交事务,并且既不用生成快照,也不用使用全局锁. 可以认为是所有分布式事务模型中性能最好的.

    不依赖数据库:不需要依赖于事务性的数据库,因为是靠预留资源来做代偿的.  也就是说不仅可以使用 mysql 这种关系型数据库,也可以使用 redis 这种非关系型数据库去实现 TCC 模式.

    缺点:

    代码侵入高:try、confirm、cancel 这三个方法需要人工编写.

    软状态,最终一致:第一阶段执行完后,直接提交事务.

    考虑幂等:将来 confirm 和 cancel 可能会执行失败,Seata 看到失败了就会重试,就可能造成死循环.  因此要考虑各种健壮性.

    1.2.3、TCC 模式注意事项:空回滚

    问题:

    在将执行某个分支事务的时候,发现执行分支事务的请求因为某种原因(网络抖动)阻塞住了,一旦阻塞的时间超过了超时时间,就会将超时的错误报告给 TC,然后 TC 就会告诉这个分支事务的 RM:“那你去回滚吧”,此时 RM 就会去执行 cancel 的业务. 

    这就导致本身你没有执行 try 预留资源,现在却要执行 cancel 去释放预留资源.  比方说 try 的业务就是去冻结 30 元的余额,但是在没有进行 try 之前却要进行释放 30 元冻结余额的业务,这不就出事了吗?

    解决方案:

    因此这里需要做一个空回滚. 

    在 try 执行请求因为某种原因阻塞时,可能会导致全局事务超时,从而先触发了 cancel 逻辑,此时根本就没有做资源预留,就不能回滚,并且也不能报错(不然 Seata 会以为 cancel 出问题了,会重试,最后导致死循环).  那么空回滚只需要我们返回一个正常结束即可.

    1.2.4、TCC 模式注意事项:业务悬挂

    问题:

    在执行完空回滚之后,try 逻辑的请求阻塞突然通畅,就会去执行资源预留业务,但是资源预留了之后就没有后续了(已经执行过 cancel 中的空回滚了),既没有 cancel,也没有 confirm,业务只执行了一半.  这就是业务悬挂.

    比如说我本来有 100 元余额,执行完空回滚后,try逻辑突然通常,冻结了我 30 元的可用余额,然后也没有后续业务了,就导致我这 30 元有是有,但是却一直用不了.

    解决办法:

    在执行 try 的时候,先判断一下是否回滚过,如果回滚过了 try 就不能执行了.  同样在执行 cancel 的时候,需要判断一下,try 是不是已经执行了,如果 try 没有执行,就去做一个空回滚.

    怎么知道 try 到底有没有执行过呢?这就需要在数据库中在创建一个表,用来记录事务的状态(记录上一步是执行了 try 呢?还是cancel?还是confirm?).  

    1.2.5、实现 TCC 模式

    案例:在用户余额扣减服务中,实现 TCC 模式.

    那么实现的思路如下:

    1. try:扣减可用余额,添加冻结金额.
    2. confirm:删除冻结金额.
    3. cancel:恢复可用金额,删除冻结金额.
    4. 注意事项:保证 confirm、cancel 接口的幂等性,注意 空回滚 和 业务悬挂.

    实现案例
    a)TCC 的 try、confirm、cancel 方法都需要在接口中基于注解来声明

    语法如下:

    1. @LocalTCC
    2. public interface TCCService {
    3. /**
    4. * Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
    5. * commitMethod 用来指定 confirm 逻辑,值必须对应自己实现的方法名. rollbackMethod 表示 cancel 逻辑,值必须对应自己实现的方法名.
    6. */
    7. @TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
    8. void prepare(@BusinessActionContextParameter(paramName = "param") String param);
    9. /**
    10. * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
    11. *
    12. * @param context 上下文,可以传递try方法的参数
    13. * @return boolean 执行是否成功
    14. */
    15. boolean confirm (BusinessActionContext context);
    16. /**
    17. * 二阶段回滚方法,要保证与rollbackMethod一致
    18. */
    19. boolean cancel (BusinessActionContext context);
    20. }

    根据上述语法,就可以编写用户余额冻结服务的接口 AccountTCCService ,如下

    1. @LocalTCC
    2. public interface AccountTCCService {
    3. /**
    4. * try:冻结指定余热
    5. * @param userId
    6. * @param money
    7. */
    8. @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    9. void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
    10. @BusinessActionContextParameter(paramName = "money") int money);
    11. /**
    12. * 删除冻结余额
    13. * @param ctx
    14. * @return
    15. */
    16. boolean confirm(BusinessActionContext ctx);
    17. /**
    18. * 删除冻结余额,恢复可用余额
    19. * @param ctx
    20. * @return
    21. */
    22. boolean cancel(BusinessActionContext ctx);
    23. }

    b)建表

    这里我们已经有了用户金额表,如下:

    这里我们还需要创建 用户冻结金额表 ,如下:

    1. CREATE TABLE `account_freeze_tbl` (
    2. `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    3. `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    4. `freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
    5. `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
    6. PRIMARY KEY (`xid`) USING BTREE
    7. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

    对应实体类如下:

    1. @Data
    2. @TableName("account_freeze_tbl")
    3. public class AccountFreeze {
    4. @TableId(type = IdType.INPUT)
    5. private String xid;
    6. private String userId;
    7. private Integer freezeMoney;
    8. private Integer state;
    9. public static abstract class State {
    10. public final static int TRY = 0;
    11. public final static int CONFIRM = 1;
    12. public final static int CANCEL = 2;
    13. }
    14. }

    c)对应刚刚上述所描述的实现思路,可以基本实现(未考虑空回滚 和 业务悬挂) 

    AccountTCCService 接口,如下:

    1. @Slf4j
    2. @Service
    3. public class AccountTCCServiceImpl implements AccountTCCService {
    4. @Autowired
    5. private AccountMapper accountMapper;
    6. @Autowired
    7. private AccountFreezeMapper freezeMapper;
    8. @Override
    9. @Transactional
    10. public void deduct(String userId, int money) {
    11. //1.获取事务 id
    12. String xid = RootContext.getXID();
    13. //2.扣减可用余额
    14. accountMapper.deduct(userId, money);
    15. //3.增加冻结金额,并记录当前事务的状态
    16. AccountFreeze freeze = new AccountFreeze();
    17. freeze.setUserId(userId);
    18. freeze.setXid(xid);
    19. freeze.setFreezeMoney(money);
    20. freeze.setState(AccountFreeze.State.TRY);
    21. freezeMapper.insert(freeze);
    22. }
    23. @Override
    24. public boolean confirm(BusinessActionContext ctx) {
    25. //1.添加事务 id
    26. String xid = RootContext.getXID();
    27. //2.根据 id 删除冻结记录
    28. int count = freezeMapper.deleteById(xid);
    29. return count == 1;
    30. }
    31. @Override
    32. public boolean cancel(BusinessActionContext ctx) {
    33. //1.查询冻结记录
    34. String xid = RootContext.getXID();
    35. AccountFreeze freeze = freezeMapper.selectById(xid);
    36. //2.恢复可用余额
    37. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    38. //3.清理冻结余额,状态修改为 cancel
    39. freeze.setFreezeMoney(0);
    40. freeze.setState(AccountFreeze.State.CANCEL);
    41. int count = freezeMapper.updateById(freeze);
    42. return count == 1;
    43. }
    44. }
    d)考虑空回滚

    考虑在执行 try 逻辑阻塞超时,执行了 cancel 逻辑,那么就需要考虑空回滚.  主要记录 cancel 状态即可.

    1. @Override
    2. public boolean cancel(BusinessActionContext ctx) {
    3. //1.查询冻结记录
    4. String xid = RootContext.getXID();
    5. AccountFreeze freeze = freezeMapper.selectById(xid);
    6. //a. 空回滚判断
    7. if (freeze == null) {
    8. //这里主要记录当前的 cancel 状态
    9. freeze = new AccountFreeze();
    10. //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了
    11. String userId = ctx.getActionContext("userId").toString();
    12. freeze.setUserId(userId);
    13. freeze.setXid(xid);
    14. freeze.setFreezeMoney(0);
    15. freeze.setState(AccountFreeze.State.TRY);
    16. freezeMapper.insert(freeze);
    17. }
    18. //2.恢复可用余额
    19. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    20. //3.清理冻结余额,状态修改为 cancel
    21. freeze.setFreezeMoney(0);
    22. freeze.setState(AccountFreeze.State.CANCEL);
    23. int count = freezeMapper.updateById(freeze);
    24. return count == 1;
    25. }

    e)幂等问题

    第一次超时了,进行空回滚(添加 freeze,设置状态为 cancel),第二次又超时了,freeze 不为空,就会进行恢复金额逻辑.  这就出问题了,不能进行恢复金额操作,因此,这里需要进行判断,如果处理过了,直接返回 true 即可.

    1. @Override
    2. public boolean cancel(BusinessActionContext ctx) {
    3. //1.查询冻结记录
    4. String xid = RootContext.getXID();
    5. AccountFreeze freeze = freezeMapper.selectById(xid);
    6. //a. 空回滚判断
    7. if (freeze == null) {
    8. //这里主要记录当前的 cancel 状态
    9. freeze = new AccountFreeze();
    10. //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了
    11. String userId = ctx.getActionContext("userId").toString();
    12. freeze.setUserId(userId);
    13. freeze.setXid(xid);
    14. freeze.setFreezeMoney(0);
    15. freeze.setState(AccountFreeze.State.TRY);
    16. freezeMapper.insert(freeze);
    17. }
    18. //b.幂等问题:第一次超时了,进行空回滚,第二次又超时了,freeze 不为空,就会进行恢复金额逻辑(这就出问题了).
    19. if(freeze.getState() == AccountFreeze.State.CANCEL) {
    20. //已经处理过依次 cancel 了,无需重复处理
    21. return true;
    22. }
    23. //2.恢复可用余额
    24. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    25. //3.清理冻结余额,状态修改为 cancel
    26. freeze.setFreezeMoney(0);
    27. freeze.setState(AccountFreeze.State.CANCEL);
    28. int count = freezeMapper.updateById(freeze);
    29. return count == 1;
    30. }

    confirm 为什么不考虑幂等了?

    因为 confirm 逻辑是删除冻结记录,底层就是 sql 调用 delete.  因此即使操作多次,也无妨.

    f)业务悬挂问题

    处理过 cancel 之后,就没必要再处理 try 了,因此这里只需要判断 freeze 是否存在冻结记录,如果有,拒绝即可.

    1. @Override
    2. @Transactional
    3. public void deduct(String userId, int money) {
    4. //1.获取事务 id
    5. String xid = RootContext.getXID();
    6. //a. 业务悬挂问题处理:判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过,要拒绝业务
    7. AccountFreeze oldFreeze = freezeMapper.selectById(xid);
    8. if(oldFreeze != null) {
    9. return;
    10. }
    11. //2.扣减可用余额
    12. accountMapper.deduct(userId, money);
    13. //3.增加冻结金额,并记录当前事务的状态
    14. AccountFreeze freeze = new AccountFreeze();
    15. freeze.setUserId(userId);
    16. freeze.setXid(xid);
    17. freeze.setFreezeMoney(money);
    18. freeze.setState(AccountFreeze.State.TRY);
    19. freezeMapper.insert(freeze);
    20. }

    g)到此,整个业务完成.

    全代码如下:

    1. @Slf4j
    2. @Service
    3. public class AccountTCCServiceImpl implements AccountTCCService {
    4. @Autowired
    5. private AccountMapper accountMapper;
    6. @Autowired
    7. private AccountFreezeMapper freezeMapper;
    8. @Override
    9. @Transactional
    10. public void deduct(String userId, int money) {
    11. //1.获取事务 id
    12. String xid = RootContext.getXID();
    13. //a. 业务悬挂问题处理:判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过,要拒绝业务
    14. AccountFreeze oldFreeze = freezeMapper.selectById(xid);
    15. if(oldFreeze != null) {
    16. return;
    17. }
    18. //2.扣减可用余额
    19. accountMapper.deduct(userId, money);
    20. //3.增加冻结金额,并记录当前事务的状态
    21. AccountFreeze freeze = new AccountFreeze();
    22. freeze.setUserId(userId);
    23. freeze.setXid(xid);
    24. freeze.setFreezeMoney(money);
    25. freeze.setState(AccountFreeze.State.TRY);
    26. freezeMapper.insert(freeze);
    27. }
    28. @Override
    29. public boolean confirm(BusinessActionContext ctx) {
    30. //1.添加事务 id
    31. String xid = RootContext.getXID();
    32. //2.根据 id 删除冻结记录
    33. int count = freezeMapper.deleteById(xid);
    34. return count == 1;
    35. }
    36. @Override
    37. public boolean cancel(BusinessActionContext ctx) {
    38. //1.查询冻结记录
    39. String xid = RootContext.getXID();
    40. AccountFreeze freeze = freezeMapper.selectById(xid);
    41. //a. 空回滚判断
    42. if (freeze == null) {
    43. //这里主要记录当前的 cancel 状态
    44. freeze = new AccountFreeze();
    45. //这里能拿到 userId 和 money 是因为在 AccountTCCService 接口中通过 BusinessActionContextParameter 注解注册了
    46. String userId = ctx.getActionContext("userId").toString();
    47. freeze.setUserId(userId);
    48. freeze.setXid(xid);
    49. freeze.setFreezeMoney(0);
    50. freeze.setState(AccountFreeze.State.TRY);
    51. freezeMapper.insert(freeze);
    52. }
    53. //b.幂等问题:第一次超时了,进行空回滚,第二次又超时了,freeze 不为空,就会进行恢复金额逻辑(这就出问题了).
    54. if(freeze.getState() == AccountFreeze.State.CANCEL) {
    55. //已经处理过依次 cancel 了,无需重复处理
    56. return true;
    57. }
    58. //2.恢复可用余额
    59. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    60. //3.清理冻结余额,状态修改为 cancel
    61. freeze.setFreezeMoney(0);
    62. freeze.setState(AccountFreeze.State.CANCEL);
    63. int count = freezeMapper.updateById(freeze);
    64. return count == 1;
    65. }
    66. }

    1.2、Saga 模式

    1.2.1、Saga 模式理论

    Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:

    第一阶段:

    与 AT 一样,直接提交本地事务.

    第二阶段:

    如果第一阶段大家都成功了,就什么也不做.

    如果第一阶段有失败的,那么他会反向做一个补偿逻辑去回滚.  这里确实和 tcc 优点像,但不完全一样,因为  tcc 再第一阶段中不是处理事务,只是做资源预留.

    比如 扣余额业务,TCC 就直接冻结了,而 saga 是直接把余额扣掉了,如果 saga 第一阶段出现问题,第二阶段就是把扣掉的余额增加回来,实现回滚逻辑的.

    1.2.2、saga 模式优缺点

    缺点:

    没有隔离性:因为一二阶段既没有全局锁,也没有预留资源,所有事务与事务之间可能存在脏写问题.

    软状态持续时间不确定:saga 模式是按顺序执行每一个事务,如果有任何一个出现问题,就会立刻反向补偿. 因此这个不一致的时间不确定.

    优点:

    吞吐能力高:基于事件驱动实现异步调用,也就是一个事务完成了,自己执行下一个事务,无需阻塞等待.

    性能高:第一阶段无需上锁,性能高.

    实现简单:不用像 TCC 那样编写三个阶段,实现简单.

    1.2.3、补充说明

    Ps:由于这种模式的使用场景极少,因此就不演示了.

  • 相关阅读:
    「专题速递」AR协作、智能NPC、数字人的应用与未来
    Shiro加入到Maven项目
    Linux Shell脚本:从文件中读取文件路径并以相同目录结构复制到当前目录
    关于俄罗斯游戏玩家特征的几个事实
    C语言每日一题(26)移除链表元素
    shell动态生成.sql文件的方法进阶2
    VUE(混入mixin、计算属性computed、监听watch)
    云原生Kubernetes:K8S安全机制
    基于SpringBoot+微信小程序的智慧医疗线上预约问诊小程序
    【无标题】
  • 原文地址:https://blog.csdn.net/CYK_byte/article/details/133580617