• springcloud3 分布式事务解决方案seata之TCC模式6


    TCC模式

    1.1 TCC的逻辑

    TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC需要人工干预编写代码。需要实现三个方法:

    1.Try:资源的检测和预留;

    2.Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

    3.Cancel:预留资源释放,可以理解为try的反向操作。

    代码地址: https://gitee.com/jurf-liu/seata-demo.git

    10-动手实践-TCC模式原理_哔哩哔哩_bilibili

    1.2  流程图

     TCC的执行流程可以分为两个阶段,分别如下:

    1.第一阶段:Try,业务系统做检测并预留资源 (加锁,锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住。

    2.第二阶段:根据第一阶段的结果决定是执行confirm还是cancel

    Confirm:执行真正的业务(执行业务,释放锁)
    Cancle:是对Try阶段预留资源的释放(出问题,释放锁)
    原文链接:https://blog.csdn.net/a745233700/article/details/122402303

    1.3 优缺点

    TCC的优点:

    1.一阶段完成直接提交事务,释放数据库资源,性能好

    2.相比AT模型,无需生成快照,无需使用全局锁,性能最强

    3.不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

    TCC的缺点是什么

    1.有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦

    2.软状态,事务是最终一致

    3.需要考虑Confirm和Cancel的失败情况,做好幂等处理

    1.4  实现空回滚与悬挂

    1.4.1 空回滚(允许空回滚)

    当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作(后发先至),这时cancel进行回滚操作,就是空回滚。

    解决办法:在cancel操作时,应当判断try操作是否已执行,如果未执行,则cacel操作就是空回滚。

    1.4.2 空悬挂(防止空悬挂)

    对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。

    执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

    二  利用TCC实现分布式事务的案例操作

    2.1 工程结构

    代码地址:https://gitee.com/jurf-liu/seata-demo.git 

    2.2 需求的描述

    1.修改account-service,编写try,confirm,cancel逻辑;

    2.try业务: 添加冻结金额,扣减可用金额;

    3.confirm业务:删除冻结金额

    4.cancel业务:修改冻结金额为0,恢复可用金额

     2.3 附件数据库

    2.4 编写try,confirm,cancel方法

    1.在account-service模块,新增接口

    1. package cn.itcast.account.service;
    2. import io.seata.rm.tcc.api.BusinessActionContext;
    3. import io.seata.rm.tcc.api.BusinessActionContextParameter;
    4. import io.seata.rm.tcc.api.LocalTCC;
    5. import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
    6. @LocalTCC
    7. public interface AccountTCCService {
    8. @TwoPhaseBusinessAction(name = "deductInfo", commitMethod = "confirm", rollbackMethod = "cancel")
    9. void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
    10. @BusinessActionContextParameter(paramName = "money")int money);
    11. boolean confirm(BusinessActionContext ctx);
    12. boolean cancel(BusinessActionContext ctx);
    13. }

    2.定义实现类

    1. package cn.itcast.account.service.impl;
    2. import cn.itcast.account.entity.AccountFreeze;
    3. import cn.itcast.account.mapper.AccountFreezeMapper;
    4. import cn.itcast.account.mapper.AccountMapper;
    5. import cn.itcast.account.service.AccountTCCService;
    6. import io.seata.core.context.RootContext;
    7. import io.seata.rm.tcc.api.BusinessActionContext;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.beans.factory.annotation.Autowired;
    10. import org.springframework.stereotype.Service;
    11. import org.springframework.transaction.annotation.Transactional;
    12. @Service
    13. @Slf4j
    14. public class AccountTCCServiceImpl implements AccountTCCService {
    15. @Autowired
    16. private AccountMapper accountMapper;
    17. @Autowired
    18. private AccountFreezeMapper freezeMapper;
    19. @Override
    20. @Transactional
    21. public void deduct(String userId, int money) {
    22. // 0.获取事务id
    23. String xid = RootContext.getXID();
    24. // 1.扣减可用余额
    25. accountMapper.deduct(userId, money);
    26. // 2.记录冻结金额,事务状态
    27. AccountFreeze freeze = new AccountFreeze();
    28. freeze.setUserId(userId);
    29. freeze.setFreezeMoney(money);
    30. freeze.setState(AccountFreeze.State.TRY);
    31. freeze.setXid(xid);
    32. freezeMapper.insert(freeze);
    33. }
    34. @Override
    35. public boolean confirm(BusinessActionContext ctx) {
    36. // 1.获取事务id
    37. String xid = ctx.getXid();
    38. // 2.根据id删除冻结记录
    39. int count = freezeMapper.deleteById(xid);
    40. return count == 1;
    41. }
    42. @Override
    43. public boolean cancel(BusinessActionContext ctx) {
    44. // 0.查询冻结记录
    45. String xid = ctx.getXid();
    46. AccountFreeze freeze = freezeMapper.selectById(xid);
    47. // 1.恢复可用余额
    48. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    49. // 2.将冻结金额清零,状态改为CANCEL
    50. freeze.setFreezeMoney(0);
    51. freeze.setState(AccountFreeze.State.CANCEL);
    52. int count = freezeMapper.updateById(freeze);
    53. return count == 1;
    54. }
    55. }

      2.5 解决空回滚和悬挂

    2.5.1 理论思想

    为了防止空回滚,业务悬挂,保证幂等性要求,需要新增一张表冻结表,在数据库记录冻结金额的同时,记录当前事务id和执行状态。

    try业务:记录冻结金额和事务状态到account_freeze表;扣减account表可用金额;

    confirm业务:根据xid删除account_freeze表的冻结记录;

    cancel业务: 修改account_freeze表,冻结金额为0,state为2;修改account表,恢复可用金额

    如何判断是否空回滚:

    在cancel业务中,根据xid查询account_freeze表,如果为null,则说明try还没做,需要空回滚。

    如何判断业务空悬挂:在try业务中,根据xid查询account_freeze,如果已经存在,则证明cancel已经执行,拒绝执行try业务。

    如下图所示:

     2.5.2 代码

    1.try方法

    2.concel方法

    3.代码 

    1. public class AccountTCCServiceImpl implements AccountTCCService {
    2. @Autowired
    3. private AccountMapper accountMapper;
    4. @Autowired
    5. private AccountFreezeMapper freezeMapper;
    6. @Override
    7. @Transactional
    8. public void deduct(String userId, int money) {
    9. // 0.获取事务id
    10. String xid = RootContext.getXID();
    11. //判断freeze是否有详情记录,如果有,则一定是cancel执行过,需要拒绝业务
    12. AccountFreeze oldFreeze=freezeMapper.selectById(xid);
    13. if(oldFreeze!=null){
    14. //cancel执行过,需要拒绝业务
    15. return ;
    16. }
    17. // 1.扣减可用余额
    18. accountMapper.deduct(userId, money);
    19. // 2.记录冻结金额,事务状态
    20. AccountFreeze freeze = new AccountFreeze();
    21. freeze.setUserId(userId);
    22. freeze.setFreezeMoney(money);
    23. freeze.setState(AccountFreeze.State.TRY);
    24. freeze.setXid(xid);
    25. freezeMapper.insert(freeze);
    26. }
    27. @Override
    28. public boolean confirm(BusinessActionContext ctx) {
    29. // 1.获取事务id
    30. String xid = ctx.getXid();
    31. // 2.根据id删除冻结记录
    32. int count = freezeMapper.deleteById(xid);
    33. return count == 1;
    34. }
    35. @Override
    36. public boolean cancel(BusinessActionContext ctx) {
    37. // 0.查询冻结记录
    38. String xid = ctx.getXid();
    39. String userId= ctx.getActionContext().get("userId").toString();
    40. AccountFreeze freeze = freezeMapper.selectById(xid);
    41. //1.空回滚判断,判断freeze是否为null,为null证明try没执行,需要回滚
    42. if(freeze==null){
    43. //证明try没执行,需要空回滚
    44. freeze = new AccountFreeze();
    45. freeze.setUserId(userId);
    46. freeze.setFreezeMoney(0);
    47. freeze.setState(AccountFreeze.State.CANCEL);
    48. freeze.setXid(xid);
    49. freezeMapper.insert(freeze);
    50. }
    51. //2.保持幂等性
    52. if(freeze.getState()==AccountFreeze.State.CANCEL){
    53. //处理过一次cancel方法,无序重复处理
    54. return true;
    55. }
    56. // 1.恢复可用余额
    57. accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
    58. // 2.将冻结金额清零,状态改为CANCEL
    59. freeze.setFreezeMoney(0);
    60. freeze.setState(AccountFreeze.State.CANCEL);
    61. int count = freezeMapper.updateById(freeze);
    62. return count == 1;
    63. }
    64. }

    2.6 controller的调用修改

    修改controller调用

    2.8 启动nacos,seata

    1.启动nacos

    2.启动seata

    2.7 验证

    2.7.1 初始态数据表

    2.7.2  正常下单

    1.请求: http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=1&money=50

     2.各表数据

    2.7.3 订单数大于库存数访问

    1.请求:http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=10&money=50

    存库量为:2; 请求量为:10,库存扣减发生异常,触发try异常执行cancel方法,进行回滚 

    2.查看控制台

    1.account:

     2.order

    3.storage: 发生扣减异常

     3.查看数据表:回滚执行了cancel方法,冻结表新增一条冻结数据,其他表均进行了回滚。

  • 相关阅读:
    HDLbits exercises 13(MORE CIRCUITS全部题)
    目标检测评估指标mAP:从Precision,Recall,到AP50-95【未完待续】
    Tomcat 源码分析 (Tomcat的Session管理) (十一)
    protobuf协议详解
    MIT6.824-lab4B-Sharded Key/Value Server(基于Raft的Shard KV数据库-分片KV存储器)
    Leetcode—136.只出现一次的数字【简单】
    SQL存储过程和函数
    双素数 马蹄集
    算法分析与设计CH25:回溯算法Back-Tracking——N皇后问题
    tomcat
  • 原文地址:https://blog.csdn.net/u011066470/article/details/133046671