分布式事务常见解决方案整理:二阶段、三阶段、TCC、MQ+本地事务+消息校对

分布式事务是要保证多个服务下的多个数据库操作的一致性。

本文以银行转账为例,来说明下分布式事务的常见解决方案。比如服务A需要对用户A扣款100元,服务B需要对用户B新增100元。

1、两阶段提交

两阶段提交的典型应用是spring cloud alibaba的seata。其解决分布式事务的方案如下:
1)阶段一:事务管理器TM(服务A)发起全局事务请求,事务协调器TC生产全局唯一事务XID。XID通过微服务调用链传播。因此各微服务(服务A+服务B)到TC上注册为XID中的一个分支。
2)各微服务进行事务操作,然后将事务结果返回给事务协调器TC,TC根据所有服务的结果判断是全局commit还是全局rollback。

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

参考文章:
1:SpringBoot 整合 Seata

2、三阶段提交

2.1 二阶段提交的缺点

既然已经有二阶段提交了,那为什么还需要三阶段提交呢?因此接下来需要重点理解下,二阶段提交的优点和缺点。

二阶段提交的缺点
1)同步阻塞问题:所有参与者的数据库事务都处于阻塞状态。如果某个参与者占用了某个公共资源,会导致其他服务请求该资源时也处于阻塞状态。因此不适用于高并发的情况。
2)事务协调者单点故障问题:二阶段中,事务协调者是非常重要的环节。如果在第2阶段,协调者宕机了,那么会导致相关数据库事务不能commit或rollback,会一致阻塞下去。
3)数据不一致的问题:在二阶段提交过程中,由于二阶段可能存在局部网络问题。可能存在协调者TC发出了commit或rollback请求,但是参与者未接收到。或者发出消息后,参与者宕机了等。那么也会导致相关事务未成功提交,导致存在数据不一致的问题。参与者在第二阶段超时未收到请求后,建议最好是rollback,当然这样也会存在数据不一致的情况,即本来应该是commit的情况。

2.2 三阶段提交

三阶段包含:CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段。思想基本和二阶段基本是一致的。

1、引入了CanCommit阶段:该阶段会先进行服务的预检查工作(比如下订单,会先判断库存是否足够),因此该步骤不会锁资源。

我理解引入该阶段的优点是:
1)可以提前做校验,减少了资源的锁定时间。因此可以提升并发量可以一定程度上解决二阶段中的同步阻塞问题
2)如果超时未收到第二阶段PreCommit的通知的话,会自动取消可以一定程度解决二阶段的事务协调者单点故障问题

2、PreCommit预提交阶段后,参与者引入了超时机制。如果未收到协调者发布的DoCommit信息,会超时执行commit阶段

该阶段的优点是可以一定程度上解决事务协调者单点故障的问题

可以看到三阶段提交和二阶段提交一样,依然都存在数据不一致的问题。针对这种事务异常的情况,可以在监测到事务异常时,通过脚本或者异步任务来补偿差异的信息,并进行告警

3、针对二阶段中的数据不一致问题,可以使用超时重试机制。
在这里插入图片描述

参考文章:
1、分布式两阶段提交和三阶段提交
2、分布式事务 - 两阶段提交和三阶段提交
3、七种常见分布式事务详解

3、TCC提交

TCC也可以理解为二阶段提交,不过它是基于应用层面的提交:Try Confirm Cancer。
1)准备阶段:Try,业务系统做检测并预留资源 (加锁,锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,也就是并没有进行数据库的事务操作。而是把下单的库存给锁定住,比如通过redis锁住对应资源。
2)根据第一阶段的结果决定是执行confirm还是cancel。Confirm:执行真正的业务(执行业务,进行数据库的事务操作,释放锁)。Cancel:是对Try阶段预留资源的释放(出问题,不进行数据库的事务操作,释放锁)。
在这里插入图片描述

3.1 TCC提交的优点

1、并发性能提升TCC的本质原理是把数据库的二阶段提交上升到微服务来实现,从而避免了数据库在二阶段提交中,由于锁冲突、长事务而导致的阻塞低性能问题可以一定程度上解决二阶段的同步阻塞问题

即将数据库阶段中的事务的阻塞等待,转化为了微服务间调用的阻塞。以转账为例,比如二阶段提交中服务A进行了数据库事务操作扣款了100元,接着就要等待服务B进行数据库事务操作增加100元,此时服务A的数据库事务就处于阻塞状态。而TCC提交的话,服务A执行了Confirm中的数据库的事务后,服务A的数据库事务就可以commit了,不用处于阻塞状态,如果服务B超时未成功返回的话,服务A可以再调用Cancer再进行回滚。

当然TCC的前提是默认Confirm阶段和Cancer阶段是一定可以执行成功的。

2、数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。可以解决二阶段提交中的数据不一致的问题
3、可靠性:解决了 XA 协议的协调者单点故障问题。由于主业务方的微服务一般是集群部署,由微服务发起并控制整个业务活动,可以解决二阶段提交中的事务协调者单点故障问题

3.2 TCC提交的缺点

1、对微服务的侵入性强:微服务的每个事务都必须实现try,confirm,cancel等3个方法,业务耦合度较高,提高了开发成本,今后维护改造的成本也高。

3.3 TCC提交的注意事项

1)允许空回滚:由于try的步骤可能会失败,因此允许执行空回滚;
2)防悬挂控制:针对try一致未成功执行,导致协调者发布cancel。导致参与者先收到cancel,再收到try请求。针对这种情况,需要在本地事务中记录该id已cancel了,所以再try的时候,就不能成功。
3)幂等问题:一定要考虑幂等情况。

4、MQ消息+本地事务+消息校对

MQ消息+本地事务的核心是对于调用方需要保证本地事务一致性,然后保证消息一定成功发送。其次被调用方需要保证一定能接收到消息,并且也要能保证本地事务的一致性。
在这里插入图片描述
以下以转账为例。

4.1 首先为什么要加消息队列?

加消息队列主要是考虑到以下2个问题
1、服务A调用服务B,可能时间较长,服务A一直处于阻塞状态;
2、流量不是很好控制,服务A如果是高流量的话,可能会压垮服务B;

4.2 需要注意的问题

大致步骤是:服务A先扣款100成功,然后发送消息到mq,然后服务B收到消息并加钱100,最后服务A完成调用。

4.2.1 服务A先扣款100成功了,怎么保证一定能把消息发送到mq呢?

可以考虑在服务A中加一张表:转账流水表。把扣款和写入转账流水表作为1个本地事务,扣款成功的话,就将该条转账记录的状态改为待处理

然后后台加一个定时任务,定期地查看转账流水表中是否有记录地状态时待处理,同时更新时间-当前时间大于阈值,说明这条数据一直没有收到结果,需要将其重新投入到消息队列中。这样就可以保证服务A只要扣款成功,就一定能将消息成功发送给服务B

当然如果服务B那边成功返回ACK了的话,可以将状态改为处理成功;ACK返回失败的话,可以将状态改为处理失败

4.2.2 既然已经保证了消息一定能发送出去,那服务B加钱100怎么保证幂等性呢?

可以在服务B这里也建一个转账日志表,让服务B加钱和写入转账记录为一个本地事务,保证加钱成功,就一定能成功写入。这样每次有消息来了之后,可以先看下这条流水id是否已经存在,存在的话,就不用重复消费了。

当然这里还存在一个问题,假设2个重复消息同时到了,那么还涉及到一个加锁的步骤了。比如2条流水号为202209200000001的消息都来了,那么可以先去redis里面看一下是否已存在202209200000001的锁,有的话,说明前面一个线程已经获取锁了,正在加钱并且写入转账日志表。

比如线程1先抢占到锁,那么先加好钱并写入转账日志表,然后释放锁。此时线程2再抢到锁,先判断转账日志表中是否已有这条记录,有的话,那就不要再重复操作了。如果没有的话,线程2就加钱并写入转账日志表。

4.3 引入消息校验保证最终一致性

可以使用定时任务进行消息校验/消息对账,来保证最终一致性。

参考文章:
1、转账引发数据一致性思考

猜你喜欢

转载自blog.csdn.net/xueping_wu/article/details/127143322