随着单体应用拆分以及微服务化,互联网公司的分布式事务场景已成常态,关于分布式事务的解决方案也是由来已久,比如Saga、XA、TCC、本地消息表等等,当然也有很多优秀的框架比如ByteTCC、TCC-transaction、EasyTransaction以及最近比较火的Seate,那么如何在众多方案中选择适合自己的呢,接下来我们分析下各方案的原理以及优缺点。
XA协议分为两段提交和三段提交。
准确讲XA是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA的都是数据库或者MQ,所以提起XA往往多指基于资源层的底层分布式事务解决方案。
由事务协调者和事务参与者组成。
阶段 1:准备阶段
阶段 2:提交阶段
情况一:当所有参与者均反馈 yes,提交事务,如下图
情况二:当有阶段一中任意参与者反馈no,则中断事务,回滚,如下图
总结:两阶段提交方案实现简单,也可以实现业务无侵入,协调者可以依赖的中间件也很多,比如早期重量级Weblogic、Jboss、后期轻量级Atomikos、Narayana和Bitronix,但是在实际中却很少应用,主要有以下原因:
并不是所有资源都支持XA协议(MySQL 5.0.2才支持, 5.7之前都有缺陷)
三阶段提交是在二阶段提交上的改进版本,主要是加入了超时机制。同时在协调者和参与者中都引入超时机制。
三阶段将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,以此来处理原先二阶段,参与者准备后,参与者发生崩溃或错误,导致参与者无法知晓是否提交或回滚的不确定状态所引起的延时问题。
情况一:一切正常
情况二:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即发出中断事务命令,参与者收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,都会执行中断,回滚事务
情况三:阶段 2 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即发出abort指令,参与者执行abort指令。
进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。
方案总结:
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
基于数据库的XA协议不管是两段提交还是三段提交,性能都不是很高,在高并发的互联网场景中不是很适用。那么在数据库只能保证本地事务的ACID情况下如何实现整个业务链路的原子性,从而保证数据的一致性呢?
最直接的方法就是按照逻辑依次调用服务,但出现异常怎么办?那就对那些已经成功的进行补偿,补偿成功就一致了,这种朴素的模型就是Saga。
处理流程:
每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成,顺序执行每一个子事务
每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果
Sage定义了两种恢复策略:
业内常用的两种实现方式:
定义一个命令协调器(OSO)来协调各个服务的执行,充当指挥官的角色,协调器事先知道整个事务的调用顺序,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚
以电商下单为例:
正常执行:
1、主业务方对OSO发起请求创建一笔新订单
2、OSO发出“创建订单”命令给订单服务,订单服务创建订单并将订单状态设置为"待处理";
2、OSO发出“减库存”命令给库存服务
3、OSO发出“支付”命令给支付服务
4、OSO发出“修改订单状态”命令给订单服务将订单状态置为“成功”
方案总结:
优点:
缺点:
一个可能的缺点就是需要维护协调中心,谁来维护呢。。。。而且存在协调器故障风险
在基于事件的方式中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
还是以电商下单为例:
正常执行:
1、主业务方发布创建订单的事件
2、订单服务监听创建订单事件,创建状态为“待处理”订单,并发布订单创建成功事件
3、库存服务监听订单创建成功事件,完成扣减库存,并发布扣减库存成功事件
4、支付服务监听扣减库存成功事件,完成支付,并发布支付成功事件
5、订单服务监听完成支付事件,修改订单状态为“已完成”,并发布订单修改
6、主业务方监听订单修改状态成功事件,并完成订单创建
方案总结:
优点:简单且容易理解。各参与方相互之间无直接沟通,完全解耦。这种方式比较适合整个分布式事务只有2-4个步骤的情形。
缺点:这种方式如果涉及比较多的业务参与方,则比较容易失控。各业务参与方可随意监听对方的消息,以至于最后没人知道到底有哪些系统在监听哪些消息。更悲催的是,这个模式还可能产生环形监听,也就是两个业务方相互监听对方所产生的事件。
Saga总结:
Saga是最终一致性的解决方案,所以优点是性能更高。缺点是不能保证事务隔离性,业务侵入性比较高,每个参与者都需要有回滚方案,实现起来较为复杂。
利用补偿的方式来保证最终一致性,Sage是最早也是最朴素的模型,但是Sage没办法保证数据的隔离性,于是TCC(Try-Confirm-Cancel)出现了。
在实际交易逻辑前先做业务检查、对涉及到的业务资源进行“预留”,或者说是一种“中间状态”,如果都预留成功则完成这些预留资源的真正业务处理,典型的如票务座位,电商库存等场景。
TCC事实上也是两阶段提交,只不过它将两阶段放到了代码层,它的三个阶段Try(检查,锁定资源),Confirm(确认提交),Cancel(取消提交)的方法都是需要我们自己在业务代码层实现。
以电商下单为例子,简化为创建订单,扣减库存:
假设原库存为10,购买数量为1,这阶段会更新库存为9,冻结1个库存,然后状态为“待处理”的订单
根据Try阶段执行的结果来确定是执行Commit还是Cannel
当Try阶段全部执行成功,进入Commit阶段。
这里使用的资源(库存,订单)都是Try预留的资源。在TCC事务机制中,认为在Try阶段能预留下来的资源,在Commit中一定能正确的提交。
当Try阶段有任意一个服务执行失败,进入Cannel阶段。
方案总结:
本地消息表的方案最早是ebay提出来的,事务发起方处理自身业务,同时将对其他服务的调用记录到消息表内,然后通过轮训的方式发送消息到事务内的其他服务。
这样设计可以保证业务处理和消息发送同时成功或者都不成功,保证了多个系统事务的数据一致性。
以创建订单同步生成鉴定订单为例,订单生成(包括创建订单,扣库存,优惠券,积分等等)详细流程不做细说:
为了数据的一致性,发送消息可能会失败,需要进行重试,所以消息的接收方需要支持幂等。
具体容错机制:
方案总结:
基于可靠消息的最终一致性方案实际上是消息中间件对本地消息表的封装,其他的都一样,典型的就是RocketMQ。
RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
还是以下单同步创建鉴定订单为例,事务发起方(Producer)就是订单中心 ,MQ订阅方就是鉴定服务:
Producer (订单服务)发送half消息(生成鉴定订单)至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定commit/rollback。
方案总结:
相比于本地消息表的方案来说
Seate是阿里开源的分布式中间件,以高效 并且对业务 0 侵入 的方式,解决微服务场景下面临的分布式事务问题。github:https://github.com/seata/seata
Seate支持AT(Automatic Transaction)模式,这种模式是对业务无侵入的,类似XA。与之相应的另外一种工作模式称为 MT(Manual Transaction)模式,这种模式下,分支事务需要应用自己来定义业务本身及提交和回滚的逻辑(其实就是TCC,只不过增加了中央协调者)。
AT模式是跟XA类似的两段提交,但是有本质区别,区别在于XA是数据库层面上的两段提交协议,只有两阶段都完成了才会释放锁。而Seate是在应用层实现两阶段提交,将数据库层面的一些东西提前到应用层实现,比如Undo log,事务数据镜像,行锁等等,绝大部分情况下,一阶段本地事务执行完已经释放了资源锁,除非二阶段需要回滚。
具体更细节的原理github上说的很清楚,这里就不多重复。
接下来我们通过分析Seata AT模式原理,来看看它的亮点与问题
Seata团队画了一个的详细调用流程图:
亮点:
本地事务执行完之后就可以提交释放锁,相比XA的两阶段释放来说是很大的提升
性能损耗:
一条update Sql需要执行以下过程
与TC(事务协调器)通讯获取全局事务xid
before image(执行前镜像),解析SQL,查询一次数据库
after image(执行后镜像),解析SQL,查询一次数据库
insert undo log,写一次数据库
before commit,与TC通讯,判断锁冲突
这些操作都需要一次远程通讯RPC,而且是同步的,另外undo log写入时blob字段的插入性能也是不高的,二阶段是异步的,不过也会占用系统资源。
这些过程粗略估计比单独sql执行应该要慢个三四倍~。
隔离性:
Seate默认支持的全局隔离级别是未提交读,不过也可以支持全局的已提交读。
seate有一个全局锁,由TC管理,多个seate事务的update会有排他锁,保证事务的一致性。当然,由于本地事务执行完了就提交了,也就是数据库本身释放锁了,如果另外一个事务不是seate事务,是没有全局锁的。
分布式事务的理想解决方案,就像我等男同胞眼中的择偶对象,白(业务侵入少)富(性能好)美(隔离性保证完整),but现实告诉我们,适合我们的才是最好的。
如同CAP,这三个特性是相互制衡的,往往只能满足其中两个,我们可以画一个三角约束(盗来的图。。):
基于业务补偿的Saga偏向1.2;TCC偏向2.3;本地消息表/事务型消息中间件偏向2.3;Seata/XA偏向1.3。
不同的业务场景面临的并发量,数据一致性要求不一样,我们要做的只能是根据不同的业务场景选择对我们来说最合适的方案。