• 微服务13-Seata的四种分布式事务模式


    XA模式

    XA模式分为两种情况
    提交成功:
    在这里插入图片描述

    提交失败:

    在这里插入图片描述

    具有强一致性seata相当于是在RM上做了一层封装;

    在这里插入图片描述

    XA模式
    优点
    1.事务的强一致性,只要有失败的,TC事务协调者就会发送信息让RM回滚——>满足ACID原则
    2.没有代码侵入,常用数据库都支持
    缺点
    1.第一阶段就要锁定数据库资源,但是却不提交,从而导致数据库所占用的资源不能释放(占数据库锁),性能较差
    2.依赖关系型数据库实现事务

    实现XA模式

    步骤:

    1.在yaml文件中开启XA代理模式

    2.添加@GlobalTransactional注解开启全局事务

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    AT模式

    在这里插入图片描述
    利用快照来保证事务的一致性,来进行数据回滚;

    在这里插入图片描述

    AT模式是一种最终一致的模式:因为RM资源管理器执行sql后会直接提交,那么此时如果是数据不一致的情况下,那么说明肯定是软一致,但是在阶段二时,AT模式RM资源管理器会利用快照进行数据回滚,从而保证最终一致;
    AT模式直接提交,有利于提高效率

    在这里插入图片描述

    AT模式的脏写问题(对同数据并发写的问题)

    脏写问题:造成数据空转现象

    什么是脏写:

    简而言之,就是两个事务并发执行,修改同一条数据,我第一个事务修改并且提交之后,释放DB锁资源,第二个事务想要进行回滚,那么就会导致脏写——>前一个事务修改无效

    在这里插入图片描述
    解决:对事务2造成数据空转现象进行处理

    1.事务1先获取DB锁,并且保存快照
    ——>2.然后执行业务sql,我们在提交事务前获取全局锁(防止一提交事务,释放DB锁后,其他事务立马插入获取DB锁更改sql)
    ——>3.此时全局锁会记录操作当前数据的事务,让该事务持有全局锁,然后提交事务释放DB锁
    ——>4.此时其他事务可以争夺DB锁,执行业务sql
    ——>5.然后和之前一样,它也要获取全局锁,但是全局锁此时已经被事务1拿了,所以它会进行自旋(300ms)
    ——>6.然后事务1如果此时要根据快照恢复数据,那么就需要DB锁,但是DB锁此时被其他事务拿了
    ——>7.死锁现象发生
    ——>8.还好其他事务重试失败后会释放锁资源,因为获取全局锁失败,那么后面的事务提交也进行不了
    ——>9.事务1再次拿到DB锁,可以进行快照恢复数据了;

    在这里插入图片描述

    其他事务不获取全局锁的一个情况(AT模式写隔离的实现)

    利用了CAS的思想 :

    实际上是有两份快照的:before-image、after-image

    跟cas一样,before是我们要回滚目标的状态,而after是相当于验证的一个状态,如果满足after的内容,就可以设置为before;

    如果不一样不满足的话,就会判断不能恢复回滚,那么我们可以记录异常发送警告;

    在这里插入图片描述

    总结:

    AT:在第一阶段RM直接提交事务,释放数据库资源,不需要像XA模式那样,还需要将状态返回给TC事务协调者,还利用了全局锁实现读写分离:将表执行的事务储存起来,相当于一个标识;
    并且**没有代码侵入,seata自动完成回滚和提交——>seata相当于RM资源管理器的一个代理**

    在这里插入图片描述

    实现AT模式

    数据库表中:lock_table:全局锁,undo_log:放的是快照信息

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    可以再下单途中在业务代码中加上断点的方式查看数据库表中记录的快照信息和AT模式的全局锁信息 他们会在业务结束时自动销毁清理干净

    TCC模式

    在这里插入图片描述
    在这里插入图片描述
    那我们TCC模式是怎么保证一致性?

    首先我们想想AT模式,在第一阶段,通过对数据库锁的获取完成事务,事务都是隔离的,所以有人成功有人失败,只有在二阶段完成回滚才能够保证数据的最终一致性,中间还是出现了软状态;

    TCC模式,为什么就不需要锁了呢?我们AT模式是利用全局锁来保证一致性的——>执行sql后提交前上一道全局锁,那么其他事务的sql就执行不了进行自旋,超时就释放DB锁,而TCC解决利用了每个事务都是预留资源进行处理——> 第一个事务冻结的金额和第二个事务冻结的金额是不一样的,跟其他事务是没有关系的,那么回滚事务也是跟其他事务不影响的,不需要加锁(类似Semaphore)

    简而言之就是把事务所用到的资源预留起来 等后面的结果再来判断是扣除还是释放,预留起来后数据库原表中的数据已经扣除了,所以其他的业务请求也不会有影响

    在这里插入图片描述

    TCC模式的关键在于有代码侵入:需要考虑Comfirm成功提交和Cancel数据回滚的编写

    优点:1.TCC第一阶段直接提交事务,提交完直接释放数据库资源,AT的话也是直接执行,但是使用了全局锁来保存事务操作的一个状态,保证其他事务争夺不了,XA的话第一阶段就垃圾了,不会提交sql业务,需要把状态给到TC事务协调者进行判断是否回滚还是提交(是一提交或者回滚就是全局那种);
    2.无需生成快照与全局锁,依赖的是一个补偿操作,因为事务直接提交的原则,所以其他事务是操作不到自己的,可用于非关系型数据库

    在这里插入图片描述

    TCC实现

    具体模式还得根据场景来,比如TCC,就很像Semaphore,一般来说是对一个共享资源进行操作,比如停车场的停车位,库存…,像下单服务就不适合了,因为你每次调用都是一个新的订单;

    一个事务是可以有多个模式实现的

    在这里插入图片描述

    在这里插入图片描述

    我们怎么样去判断是否空回滚和业务悬挂?

    利用两者相互判断 根据冻结金额的那张表来判断 在进行try业务前 先查询一下冻结金额的表中的数据是否为空 如果不为空则证明已经执行了CANCEL操作 则需要直接拒绝try的操作,反之在进行cancel业务前,需要根据事务id查询一下冻结金额的表中的数据是否为空 如果为空的话则证明try业务还没做,需要进行空回滚,同时也需要记录数据,new一个新的对象将冻结金额设为0,以及其他数据set进去

    业务分析

    在这里插入图片描述
    在这里插入图片描述

    我们可以在BusinessActionContext中获取里面的参数

    事务表:表示事务冻结金额,冻结金额状态发生改变——>表示那部分被锁定

    事务id,用户id,冻结金额和状态

    在这里插入图片描述

    业务方便代码的实现:

    @Slf4j
    @Service
    public class AccountTCCServiceImpl implements AccountTCCService {
    
        @Autowired
        private AccountMapper accountMapper;
    
        @Autowired
        private AccountFreezeMapper accountFreezeMapper;
    
        @Override
        @Transactional
        public void deduct(String userId, int money) {
            //0.获取事务id
            String xid = RootContext.getXID();
            // 判断是否有冻结记录 有的话直接拒绝执行try业务
            AccountFreeze Freeze = accountFreezeMapper.selectById(xid);
            if (Freeze!=null) {
                //拒绝
                return;
            }
            //1.扣减可用余额
            accountMapper.deduct(userId, money);
            //2.记录冻结金额,记录事务状态
            AccountFreeze accountFreeze = new AccountFreeze();
            accountFreeze.setUserId(userId);
            accountFreeze.setFreezeMoney(money);
            accountFreeze.setState(AccountFreeze.State.TRY);
            accountFreeze.setXid(xid);
            accountFreezeMapper.insert(accountFreeze);
    
        }
    
        @Override
        public boolean confirm(BusinessActionContext ctx) {
            //0.获取事务id
            String xid = ctx.getXid();
            //1.删除数据
            int count = accountFreezeMapper.deleteById(xid);
            return count == 1;
        }
    
        @Override
        public boolean cancel(BusinessActionContext ctx) {
            //0.查询冻结记录
            AccountFreeze accountFreeze = accountFreezeMapper.selectById(ctx.getXid());
            String userId = (String) ctx.getActionContext("userId");
            //0.2.判断是否空回滚
            if (accountFreeze == null){
                accountFreeze = new AccountFreeze();
                accountFreeze.setUserId(userId);
                accountFreeze.setFreezeMoney(0);
                accountFreeze.setState(AccountFreeze.State.CANCEL);
                accountFreeze.setXid(ctx.getXid());
                accountFreezeMapper.insert(accountFreeze);
                return true;
            }
    
            //0.3 幂等判断
            if (accountFreeze.getState()==AccountFreeze.State.CANCEL){
                //已经处理过cancel了 无需重复业务
                return true;
            }
    
            //1.恢复可用余额
            accountMapper.refund(accountFreeze.getUserId(),accountFreeze.getFreezeMoney());
    
            //2.将冻结金额清零 改状态为cancel
            accountFreeze.setFreezeMoney(0);
            accountFreeze.setState(AccountFreeze.State.CANCEL);
            int count = accountFreezeMapper.updateById(accountFreeze);
            return count == 1;
    
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    在这里插入图片描述

    Saga模式

    与TCC模式类似,但是TCC第一阶段只是将资源进行冻结,真正的去除还是在第二阶段的,而Saga模式是直接提交本地事务,第二阶段直接操作事务本身:成功则什么都不做,失败则通过编写补偿业务来进行回滚;

    与AT相比没有用锁,与TCC比没有冻结资源,性能较好;

    失败用自定义的补偿来写;

    缺点:

    没有保证隔离性,既没有隔离预留资源又没有上锁,容易出现脏写

    在这里插入图片描述

    总结

    在这里插入图片描述

  • 相关阅读:
    【es6】解决箭头函数所有的问题,箭头函数的 this 指针,使用 new 操作符
    十九、kafka消费者思考之partition leader切换会引起消费者Rebalance么?
    DeU-Net: 用于三维心脏mri视频分割的可变形(Deformable)U-Net
    Java中Long型数据类型对应MySQL数据库中哪种类型?
    Flutter更新sdk时报错
    VirtualBox7安装Ubuntu20及全屏、共享、粘贴板等设置
    vscode使用git
    Kubernetes中的核心机制
    【python海洋专题二十四】南海年平均海流图
    EXCEL VBA 入门与实用例子
  • 原文地址:https://blog.csdn.net/weixin_67201964/article/details/133802345