常用分布式事务解决方案分析

随着单体应用拆分以及微服务化,互联网公司的分布式事务场景已成常态,关于分布式事务的解决方案也是由来已久,比如Saga、XA、TCC、本地消息表等等,当然也有很多优秀的框架比如ByteTCC、TCC-transaction、EasyTransaction以及最近比较火的Seate,那么如何在众多方案中选择适合自己的呢,接下来我们分析下各方案的原理以及优缺点。

一、XA

XA协议分为两段提交和三段提交。

准确讲XA是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA的都是数据库或者MQ,所以提起XA往往多指基于资源层的底层分布式事务解决方案。

1.1 两阶段提交

由事务协调者和事务参与者组成。

阶段 1:准备阶段

  • 协调者向所有参与者发送事务内容,确认是否可以提交事务。
  • 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
  • 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交

阶段 2:提交阶段

情况一:当所有参与者均反馈 yes,提交事务,如下图

  1. 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
  2. 参与者执行 commit 请求,并释放整个事务期间占用的资源。
  3. 各参与者向协调者反馈 ack(应答)完成的消息。
  4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。

常用分布式事务解决方案分析_第1张图片

 

情况二:当有阶段一中任意参与者反馈no,则中断事务,回滚,如下图

  • 协调者向所有参与者发出回滚请求(即 rollback 请求)。
  • 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
  • 各参与者向协调者反馈 ack 完成的消息。
  • 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。

常用分布式事务解决方案分析_第2张图片

 

总结:两阶段提交方案实现简单,也可以实现业务无侵入,协调者可以依赖的中间件也很多,比如早期重量级Weblogic、Jboss、后期轻量级Atomikos、Narayana和Bitronix,但是在实际中却很少应用,主要有以下原因:

  • 性能(阻塞性协议,增加响应时间、锁时间、死锁)
  • 并不是所有资源都支持XA协议(MySQL 5.0.2才支持, 5.7之前都有缺陷)

  • 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
  • 数据一致性问题:在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

1.2 三阶段提交

三阶段提交是在二阶段提交上的改进版本,主要是加入了超时机制。同时在协调者和参与者中都引入超时机制。

三阶段将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,以此来处理原先二阶段,参与者准备后,参与者发生崩溃或错误,导致参与者无法知晓是否提交或回滚的不确定状态所引起的延时问题。

情况一:一切正常

常用分布式事务解决方案分析_第3张图片

 

情况二:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即发出中断事务命令,参与者收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,都会执行中断,回滚事务

常用分布式事务解决方案分析_第4张图片

 

情况三:阶段 2 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即发出abort指令,参与者执行abort指令。

常用分布式事务解决方案分析_第5张图片

 

进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

方案总结:

优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

二、Saga

基于数据库的XA协议不管是两段提交还是三段提交,性能都不是很高,在高并发的互联网场景中不是很适用。那么在数据库只能保证本地事务的ACID情况下如何实现整个业务链路的原子性,从而保证数据的一致性呢?

最直接的方法就是按照逻辑依次调用服务,但出现异常怎么办?那就对那些已经成功的进行补偿,补偿成功就一致了,这种朴素的模型就是Saga。

处理流程:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成,顺序执行每一个子事务

  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果

常用分布式事务解决方案分析_第6张图片

 

Sage定义了两种恢复策略:

  • 向前恢复:适用于必须要成功的场景,执行顺序是这样的T1(成功),T2(成功)... Tj(失败),Tj(重试)...Tn(成功),这种情况是不需要Ci回退策略的,只能重试。
  • 向后恢复:对应上面的第二种执行顺序,执行异常回滚

业内常用的两种实现方式:

1、中央协调(Order Orchestrator)

定义一个命令协调器(OSO)来协调各个服务的执行,充当指挥官的角色,协调器事先知道整个事务的调用顺序,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚

以电商下单为例:

正常执行:

1、主业务方对OSO发起请求创建一笔新订单

2、OSO发出“创建订单”命令给订单服务,订单服务创建订单并将订单状态设置为"待处理";

2、OSO发出“减库存”命令给库存服务

3、OSO发出“支付”命令给支付服务

4、OSO发出“修改订单状态”命令给订单服务将订单状态置为“成功”

常用分布式事务解决方案分析_第7张图片

 

方案总结:

优点:

  1. 避免了业务方之间的环形依赖。
  2. 将分布式事务的管理交由协调中心管理,协调中心对整个逻辑非常清楚。
  3. 减少了业务参与方的复杂度。这些业务参与方不再需要监听不同的消息,只是需要响应命令并回复消息。
  4. 测试更容易(分布式事务逻辑存在于协调中心,而不是分散在各业务方)。
  5. 回滚也更容易。

缺点:

一个可能的缺点就是需要维护协调中心,谁来维护呢。。。。而且存在协调器故障风险

2、事件编排(Event Choreography)

在基于事件的方式中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

还是以电商下单为例:

常用分布式事务解决方案分析_第8张图片

 

正常执行:

1、主业务方发布创建订单的事件

2、订单服务监听创建订单事件,创建状态为“待处理”订单,并发布订单创建成功事件

3、库存服务监听订单创建成功事件,完成扣减库存,并发布扣减库存成功事件

4、支付服务监听扣减库存成功事件,完成支付,并发布支付成功事件

5、订单服务监听完成支付事件,修改订单状态为“已完成”,并发布订单修改

6、主业务方监听订单修改状态成功事件,并完成订单创建

 

方案总结:

优点:简单且容易理解。各参与方相互之间无直接沟通,完全解耦。这种方式比较适合整个分布式事务只有2-4个步骤的情形。

缺点:这种方式如果涉及比较多的业务参与方,则比较容易失控。各业务参与方可随意监听对方的消息,以至于最后没人知道到底有哪些系统在监听哪些消息。更悲催的是,这个模式还可能产生环形监听,也就是两个业务方相互监听对方所产生的事件。

 

Saga总结:

Saga是最终一致性的解决方案,所以优点是性能更高。缺点是不能保证事务隔离性,业务侵入性比较高,每个参与者都需要有回滚方案,实现起来较为复杂。

三、TCC

利用补偿的方式来保证最终一致性,Sage是最早也是最朴素的模型,但是Sage没办法保证数据的隔离性,于是TCC(Try-Confirm-Cancel)出现了。

在实际交易逻辑前先做业务检查、对涉及到的业务资源进行“预留”,或者说是一种“中间状态”,如果都预留成功则完成这些预留资源的真正业务处理,典型的如票务座位,电商库存等场景。

TCC事实上也是两阶段提交,只不过它将两阶段放到了代码层,它的三个阶段Try(检查,锁定资源),Confirm(确认提交),Cancel(取消提交)的方法都是需要我们自己在业务代码层实现。

以电商下单为例子,简化为创建订单,扣减库存:

1、Try阶段

假设原库存为10,购买数量为1,这阶段会更新库存为9,冻结1个库存,然后状态为“待处理”的订单

常用分布式事务解决方案分析_第9张图片

 

  • 完成所有业务检查
  • 预留必须业务资源( 准隔离性 )
  • Try 尝试执行业务

2、Commit/Cannel阶段

根据Try阶段执行的结果来确定是执行Commit还是Cannel

常用分布式事务解决方案分析_第10张图片

 

当Try阶段全部执行成功,进入Commit阶段。

这里使用的资源(库存,订单)都是Try预留的资源。在TCC事务机制中,认为在Try阶段能预留下来的资源,在Commit中一定能正确的提交。

常用分布式事务解决方案分析_第11张图片

 

当Try阶段有任意一个服务执行失败,进入Cannel阶段。

方案总结:

  • 利用 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
  • 不需要中间协调者,完全依赖业务调用方来控制,避免了协调者的故障问题
  • 业务侵入性太高,开发成本较高。

四、本地消息表

本地消息表的方案最早是ebay提出来的,事务发起方处理自身业务,同时将对其他服务的调用记录到消息表内,然后通过轮训的方式发送消息到事务内的其他服务。

这样设计可以保证业务处理和消息发送同时成功或者都不成功,保证了多个系统事务的数据一致性。

以创建订单同步生成鉴定订单为例,订单生成(包括创建订单,扣库存,优惠券,积分等等)详细流程不做细说:

常用分布式事务解决方案分析_第12张图片

 

  1. 订单服务:1、创建订单;2、将要发送给鉴定服务的消息存储到消息表
  2. 订单服务通过消息中间件,将消息发送给鉴定服务
  3. 鉴定服务创建鉴定订单,并将结果以消息的方式发送给订单服务
  4. 订单服务修改消息表状态为已完成

为了数据的一致性,发送消息可能会失败,需要进行重试,所以消息的接收方需要支持幂等。

具体容错机制:

  1. 业务异常,如果需要异常回滚(事实上鉴定服务没那么重要,不需要回滚订单,不过如果换成库存服务就需要了~),事务发起方接收到异常响应之后需要发起回滚消息到需要回滚的事务参与方,事务参与方回滚事务。
  2. 网络异常,消息发送失败 ,事务发起方进行重试,这就是消息表的作用,可以设置定时任务对未处理完的消息进行重新发送,消费方需要加幂等。

方案总结:

  • 利用消息表来保证MQ的可靠性,从而保证事务的一致性
  • 方案比较轻量,也容易实现
  • 保证最终一致性
  • 业务侵入性高,而且需要对每个场景设置回滚方法
  • 消息表使用业务系统数据库,占用业务系统资源,同时也受限于业务数据库的并发

五、可靠消息

基于可靠消息的最终一致性方案实际上是消息中间件对本地消息表的封装,其他的都一样,典型的就是RocketMQ。

RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。

常用分布式事务解决方案分析_第13张图片

 

还是以下单同步创建鉴定订单为例,事务发起方(Producer)就是订单中心 ,MQ订阅方就是鉴定服务:

  1. Producer (订单服务)发送half消息(生成鉴定订单)至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。

  2. MQ Server回应half消息发送成功
  3. Producer执行本地事务(创建订单)
  4. 消息投递,Producer根据本地事务执行结果决定发送二次确认(commit/rollback)到MQ Server
  5. 如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定commit/rollback。

方案总结:

相比于本地消息表的方案来说

  • 消息数据独立存储,降低了于业务系统的耦合
  • 并发量不被业务数据库影响,由MQ Server自身决定
  • 每次消息发送需要至少两次网络请求,因为有half消息和二次确认(commit/rollback)
  • 事务发起方需要实现消息回查的方法

六、Seate

Seate是阿里开源的分布式中间件,以高效 并且对业务 0 侵入 的方式,解决微服务场景下面临的分布式事务问题。github:https://github.com/seata/seata

6.1 设计初衷

  • 无业务入侵:引入分布式事务,不能影响业务。比如,tcc,saga,消息表之类的人工补偿是实践起来实在有点麻烦
  • 高性能:体现在一阶段本地事务执行完,就commit释放数据库锁,但是依赖seate的事务,还是持有全局锁

6.2 设计原理

Seate支持AT(Automatic Transaction)模式,这种模式是对业务无侵入的,类似XA。与之相应的另外一种工作模式称为 MT(Manual Transaction)模式,这种模式下,分支事务需要应用自己来定义业务本身及提交和回滚的逻辑(其实就是TCC,只不过增加了中央协调者)。

AT模式是跟XA类似的两段提交,但是有本质区别,区别在于XA是数据库层面上的两段提交协议,只有两阶段都完成了才会释放锁。而Seate是在应用层实现两阶段提交,将数据库层面的一些东西提前到应用层实现,比如Undo log,事务数据镜像,行锁等等,绝大部分情况下,一阶段本地事务执行完已经释放了资源锁,除非二阶段需要回滚。

 

具体更细节的原理github上说的很清楚,这里就不多重复。

6.3、方案分析

接下来我们通过分析Seata AT模式原理,来看看它的亮点与问题

Seata团队画了一个的详细调用流程图:

常用分布式事务解决方案分析_第14张图片

 

亮点:

  • 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
  • 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
  • 通过全局锁(事务协调器管理)实现了写隔离与读隔离。
  • 本地事务执行完之后就可以提交释放锁,相比XA的两阶段释放来说是很大的提升

性能损耗:

一条update Sql需要执行以下过程

  • 与TC(事务协调器)通讯获取全局事务xid

  • before image(执行前镜像),解析SQL,查询一次数据库

  • 执行update sql
  • after image(执行后镜像),解析SQL,查询一次数据库

  • insert undo log,写一次数据库

  • before commit,与TC通讯,判断锁冲突

  • commit,异步执行,删除镜像,undo log,释放锁

这些操作都需要一次远程通讯RPC,而且是同步的,另外undo log写入时blob字段的插入性能也是不高的,二阶段是异步的,不过也会占用系统资源。

这些过程粗略估计比单独sql执行应该要慢个三四倍~。

隔离性:

Seate默认支持的全局隔离级别是未提交读,不过也可以支持全局的已提交读。

seate有一个全局锁,由TC管理,多个seate事务的update会有排他锁,保证事务的一致性。当然,由于本地事务执行完了就提交了,也就是数据库本身释放锁了,如果另外一个事务不是seate事务,是没有全局锁的。

七、总结

分布式事务的理想解决方案,就像我等男同胞眼中的择偶对象,白(业务侵入少)富(性能好)美(隔离性保证完整),but现实告诉我们,适合我们的才是最好的常用分布式事务解决方案分析_第15张图片

如同CAP,这三个特性是相互制衡的,往往只能满足其中两个,我们可以画一个三角约束(盗来的图。。):

常用分布式事务解决方案分析_第16张图片

 

基于业务补偿的Saga偏向1.2;TCC偏向2.3;本地消息表/事务型消息中间件偏向2.3;Seata/XA偏向1.3。

不同的业务场景面临的并发量,数据一致性要求不一样,我们要做的只能是根据不同的业务场景选择对我们来说最合适的方案。

 

你可能感兴趣的:(java,分布式)