vlambda博客
学习文章列表

seata(一) 分布式事务解决方案汇总

概述

分布式事务是老生长谈的话题了,过去也一直没有很好的解决办法,没有很方便使用的架构。不过现在阿里开源了seata, 笔者正在学习中,顺便记录一下笔记。

本文先来梳理一下目前流行的分布式事务的各种解决方案,以及他们的对比分析。

什么是分布式事务

事务: 由一组操作构成的可靠的独立的工作单元,事务具备ACID的特性,即原子性、一致性、隔离性和持久性。

原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

本地事务:当事务由资源管理器本地管理时被称作本地事务。本地事务的优点就是支持严格的ACID特性,高效,可靠,状态可以只在资源管理器中维护,而且应用编程模型简单。最简单的例子就是MySQL在没有分库分表的时候,那么在单机部署MySQL实例上运行的事务就是本地事务(单机事务)。



分布式事务:当事务由全局事务管理器进行全局管理时成为全局事务,事务管理器负责管理全局的事务状态和参与的资源,协同资源的一致提交回滚。

目前流行的微服务架构可以处理海量请求,支持平滑扩容缩容,灵活开发部署升级等优点,但是也带来了分布式锁事务问题。比如支付下单,通常将扣款服务和订单服务,库存服务等等拆分开作为微服务部署,但是下单操作需要保证他们都成功后者都失败才可以,那么这就是分布式事务的典型场景。

分布式事务为什么难

先介绍两个基本理论

CAP理论

CAP理论指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。CAP原则的精髓就是要么AP,要么CP,要么AC,同时满足CAP的系统是无法实现的。seata(一) 分布式事务解决方案汇总那么放弃CAP中任意一个会怎么样的?

放弃P(CA):如果希望能够避免系统出现分区容错性问题,一种较为简单的做法就是将所有的数据(或者是与事物先相关的数据)都放在一个分布式节点上,这样虽然无法保证100%系统不会出错,但至少不会碰到由于网络分区带来的负面影响

放弃A(CP): 其做法是一旦系统遇到网络分区或其他故障时,那受到影响的服务需要等待一定的时间,应用等待期间系统无法对外提供正常的服务,即不可用

放弃C(AP): 这里说的放弃一致性,并不是完全不需要数据一致性,是指放弃数据的强一致性,保留数据的最终一致性。



具体采用哪种策略就要看具体的业务了。

base理论

BASE是基本可用,软状态,最终一致性。是对CAP中一致性和可用性权衡的结果,是基于CAP定理演化而来的,核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特定,采用适当的方式来使系统达到最终一致性. 也就是上一节讲的放弃了C。

基本可用(BasicallyAvailable):指分布式系统在出现故障时,允许损失部分的可用性来保证核心可用。软状态(SoftState):指允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。最终一致性(EventualConsistency):指分布式系统中的所有副本数据经过一定时间后,最终能够达到一致的状态。

一致性模型

强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。

分布式事务就是为了解决上面提到的各种问题。综上所述,由于微服务,分布式系统下,CAP无法完全同时满足,而且出于性能, 可用性,不同业务对一致性的不同要求等,想要一种大一统的分布式事务解决方案几乎不可能,所以市面上出现了很多分布式事务的解决方案,接下来逐个分析一下,并对比他们的优缺点及适用场景。

分布式事务解决方案

2PC:两阶段提交

二阶段提交协议主要分为来个阶段:准备阶段和提交阶段。2PC是一个非常经典的强一致、中心化的原子提交协议 在无任何异常的情况下,整体流程是这样的:seata(一) 分布式事务解决方案汇总

当某个参与者在prepare阶段返回失败,则需要回滚:seata(一) 分布式事务解决方案汇总

值得注意的是,二阶段提交协议的第一阶段准备阶段不仅仅是回答YES or NO,还是要执行事务操作的,只是执行完事务操作,并没有进行commit还是roolback。也就是说,一旦事务执行之后,在没有执行commit或者roolback之前,资源是被锁定的。这会造成阻塞。

2PC 存在的问题

2PC存在几个致命的缺点,所以没有人会直接使用2PC协议。缺点如下:

同步阻塞(严重的性能问题)。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源(prepare阶段会锁定资源并执行事务)时,其他第三方节点访问公共资源不得不处于阻塞状态。单点故障。由于协调者是2PC的核心,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

2PC的核心点在于,一个参与者的状态只有他自己和协调者知道,其他参与者不知道!

接下来详细分析一下:

协调者在第一阶段挂掉,参与者均正常 只需要协调者重启(前提是已经解决了单点问题),然后重新询问一下最后一个事务的状态即可。可以保证一致性。

协调者在第二阶段挂掉,参与者均正常* 由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况。

解决方案*:首先解决协调者单点问题,设置一个备份,并且要记录事务当前执行的状态日志,然后主备协调者需要同步事务执行状态。此时,主协调者宕机,启用备协调者,备协调者根据日志已经知道最后一个事务应该执行commit还是rollback。然后再去询问各个参与者的状态,就可以保证一致性了。

参与者在第一阶段挂掉,协调者正常 如果挂了以后,没有再恢复,则协调者收不到该参与者的回应,整体事务回滚,不会由一致性问题;如果挂了以后又恢复了,该参与者知道自己的整体,协调者知道整体的整体,参与者只需要询问协调者接下来该怎么做即可,不会有一致性问题。

参与者在第二阶段挂掉,协调者正常* 挂了,没再恢复:如果协调者发出的是rollback请求,不会有一致性问题;如果协调者发出的commit请求,则其他参与者commit执行了,挂掉的参与者没有执行,会有数据不一致的问题。挂了,又恢复了:只需要去询问应该commit还是rollback即可,没有一致性问题。

部分参与者和协调者都挂了 又分三种情况

第一种:在第一阶段挂 挂掉的协调者重启(或启用了备协调者)并且协调者也重启后,可以询问所有参与者的状态,然后进行相应处理,不会又数据一致性问题。(如果此时挂的参与者无法重启,则事务会全局回滚。)

第二种:在第二阶段挂,参与者在挂之前未收到协调者的指令或者收到还未来得及执行 如果协调者发出的是commit,其他参与者已执行,参与者恢复后询问协调者,会收到协调者的commit指令(实际情况可能是该参与者需要将两个阶段都执行一下,因为宕机后第一阶段执行的未提交事务丢失了),此时不会有数据一致性问题。但是如果参与者宕机后无法恢复,则会一直阻塞。如果协调者发出的是rollback,且其他参与者收到了rollback指令,则无论参与者是否恢复,都不会有数据一致性问题。如果在协调者挂的时候,其他参与者都没有收到协调者的指令,那么他们就不知道该如何处理了,只能阻塞等待, 锁定的资源也不会释放。除非协调者恢复了。

第三种:在第二阶段挂,参与者在挂之前已收到协调者的指令并且执行。此时协调者和参与者恢复后,此时协调者知道自己是应该commit还是rollback,然后询问参与者的状态,并且将未成功执行指令的参与者重发指令即可。注意这种情况会存在长时间的数据不一致,时间长短取决于宕机服务恢复的时间。




可以看到,以上几种情况都依赖于已经解决了单点问题。如果没有解决单点,则基本都是数据不一致情况。而且,如果协调者宕机后,不是原来的协调者恢复,而是新的协调者恢复,那么新的协调者可能会没有那个异常事务的一些信息(注意: 上面提到的新的协调者启动后可以获取到当前事务的状态,是在已经解决了单点,并且实现了协调者集群之间的数据一致性。但是2PC并没有实现这两个功能),它不知道该回滚还是提交,那么此时就会出现异常了,如图:

seata(一) 分布式事务解决方案汇总上图中,协调者只给参与者C发送了commit指令,但是参与者C和协调者此时都宕机了。那么参与者A和B都不知道该怎么办,只能阻塞等待。而且如果是一台新的协调者启动,但是没有那台宕机协调者的数据,那么新的协调者不知道该commit还是rollback。

3PC:三阶段提交

3PC是2PC的升级版本,顾名思义分为三个阶段: CanCommit, preCommit, commit 与两阶段提交不同的是,三阶段提交有两个改动点。

引入超时机制。同时在协调者和参与者中都引入超时机制(2PC中只有协调者可以超时,参与者没有超时机制)。协调者超时会发出中断指令;参与者超时会默认提交!3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

seata(一) 分布式事务解决方案汇总

cancommit

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

precommit

协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。事务预提交 参与者接收到PreCommit请求后,会执行事务操作。响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断

发送中断请求 协调者向所有参与者发送abort请求。中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

commit

该阶段进行真正的事务提交,也可以分为以下两种情况。


执行提交

发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。响应反馈 事务提交完之后,向协调者发送Ack响应。完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

发送中断请求 协调者向所有参与者发送abort请求事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

3PC 相对于2PC的优缺点

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。seata(一) 分布式事务解决方案汇总这里还是以上图demo为例。在2PC没有解决单点的情况下3PC如何处理这种情况。

我们假设挂掉的那台参与者执行的操作是commit。那么其他没挂的操作者的状态应该是什么?他们的状态要么是prepare-commit要么是commit。因为3PC的第三阶段一旦有机器执行了commit,那必然第一阶段大家都是同意commit。所以,这时,新选举出来的协调者一旦发现未挂掉的参与者中有人处于commit状态或者是prepare-commit的话,那就执行commit操作。否则就执行rollback操作。这样挂掉的参与者恢复之后就能和其他机器保持数据一致性了。

但是这种机制也会导致数据一致性问题,新选出来的协调者发现了有协调者是commit,但是这个commit不是老协调者发出的,是由于超时commit的,那么此时还是不能完全确定现在到底应该commit还是rollback。或者还有这种情况,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

所以要使用2PC或3PC必须解决单点及多个协调者之间的数据一致性问题,以协调者的数据为准才行。

XA

XA规范:seata(一) 分布式事务解决方案汇总XA规范中分布式事务有AP,RM,TM组成:

其中应用程序(Application Program ,简称AP):AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。 资源管理器(Resource Manager,简称RM):Rm管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库、文件系统、打印机服务器等。

事务管理器(Transaction Manager ,简称TM):负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。

Xa主要规定了RM与TM之间的交互。XA协议使用的是两阶段协议。所以2阶段协议存在的问题,XA也存在。数据一致性的问题可以通过改为3PC,解决单点问题,解决协调者集群之间的数据强一致性问题,比如使用raft或者paxos协议等。 XA最严重的问题是锁定资源,性能低下,所以实际的生产过程中,应用范围比较小。

seata(一) 分布式事务解决方案汇总

TCC

TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。

Try:预留(锁定)业务资源/数据效验 Try的锁定资源实现了一定程度的隔离性。但由于下面两个阶段都是在不同的服务(具体对应在服务下面的数据库)上执行每个分支的事务,所以不是完全意义上的隔离。Confirm:确认执行业务操作 Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。Cancel:取消执行业务操作

TCC是采用补偿机制的两阶段编程模型。

seata(一) 分布式事务解决方案汇总2PC的实现通常都是在跨库的DB层面(如XA),而TCC则在应用层面处理,需要通过业务逻辑实现。TCC 的优点是 理论简单,性能相对XA有大幅提升,因为TCC让应用自己定义数据库操作的粒度,将一个大的分布式事务拆分为多个小的本地事务,使得降低锁冲突、提高吞吐量成为可能。但是缺点很明显,业务侵入性高,每个服务都需要提供confirm和cancal接口。另外实现复杂度高,需要处理各种异常情况,如幂等,空回滚,资源悬挂等等。并且隔离性也比较弱。

这些具体的问题在下一篇文章分析seata的时候具体分析,看seata是如何解决这些问题的。

MQ事务消息(可靠消息)

该方案通过MQ消息来保证最终一致性。目前RocketMQ是支持事务消息的。

RocketMQ是基于2PC协议基础之上演变而来的,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息。

seata(一) 分布式事务解决方案汇总分为两个逻辑:正常事务消息的发送及提交、事务消息的补偿流程

事务消息发送及提交:

1.发送消息(half消息)2.服务端响应消息写入结果3.根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)4.根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

补偿流程:

1.对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”2.Producer收到回查消息,检查回查消息对应的本地事务的状态3.根据本地事务状态,重新Commit或者Rollback

补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

当然消费者端在消费时要保证幂等。

本地事务消息表(可靠消息)

这是一种常见的使用策略,避免了分布式事务,将分布式事务拆分为多个本地事务,采用最终一致性方案来实现。

上图中的消息队列Kafka不是必须的,可以通过消息传递,也可以直接通过RPC或者HTTP调用对方服务。

假设使用MQ的情况下的处理流程(假设有两个子事务A和B, A先执行):

1.子事务A将业务数据和事务消息作为一个本地事务一同提交,发起事务。2.消息经过MQ发送到服务B,服务A等待处理结果(注意这里是异步的,无需阻塞)。3.服务B接收消息,完成业务逻辑并通知服务A已处理的消息,服务A根据收到的消息修改自己本地事务表中的状态 。

容错处理情况如下:

1.当步骤1处理出错,事务回滚,相当于什么都没有发生。2.当步骤2、3处理出错,由于消息保存在消费者表中,可以重新发送到MQ进行重试。这里就需要有定时器来扫描处于异常状态的事务消息。3.如果步骤3处理出错,且是业务上的失败,服务B发送消息通知服务A事务失败,服务A需要回滚自己的本地消息。

注意,事务双方都需要支持幂等。如定时器扫描到异常状态的事务消息,则需要重新给事务的被动方发送消息。

优点:从应用设计开发的角度实现了分布式事务。原理简单,实现简单,不宜出错。缺点:与具体的业务场景绑定,耦合性强,不可公用。事务日志数据与业务数据同库,占用业务系统资源。且需要额外开发定时扫描校验任务,会给系统造成额外消耗。当长事务场景整体链路会比较长,数据不一致时间长,出问题概率大。

最大努力通知

最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。

最大努力通知型的实现方案,一般符合以下特点:

不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,如果通知失败则按照规则继续多次通知,直到通知N次后不再通知,允许消息丢失(不可靠消息)。定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。这里业务查询与校对系统时额外的成本。主被动方影响关系:被动方的处理结果不影响主动方的处理结果。
如某短信平台的设计。上图来自于https://www.jianshu.com/p/24deaea53875

最大努力通知与可靠消息一致性有什么不同?

解决方案思想不同

  可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。

  最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。

两者的业务应用场景不同

  可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。

  最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。

技术解决方向不同

  可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。

  最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

AT

AT 模式下,事务管理器把每个服务(数据库)被当做是一个 Resource。然后监听他们要执行的SQL,为每条SQL语句记录两个快照:beforeImage和afterImage。分别记录了修改前后的字段值,然后在需要回滚时,就使用beforeImage中记录的值来回复原始数据即可。

这跟MySQL本身的undolog有异曲同工之妙。类似于MySQL单机事务的处理流程。

AT模式相比XA模式,性能有很大提升,因为AT模式依然是将事务的管理由数据库提升到了应用组件中,锁的范围由整个分布式事务将为单个子事务。

seata的AT模式实现及原理请参考笔者文章 分布式事务框架seata1.3 AT及XA模式实例演示[1]

SAGA

1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理long lived transaction(长活事务)。Saga是一个长活事务可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。

该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。

Saga的组成

每个Saga由一系列sub-transaction Ti 组成每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果

可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

T1, T2, T3, ..., TnT1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n

Saga的恢复策略:

backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。

这里先对saga的概念有个简单的认识,后面对seata的saga模式进行分析时再深入介绍。

对账

在一些与外部企业交互过程中,可能不会那么方便的解决分布式事务问题,比如不支持各种含有MQ的方案等等。对方只提供了简单的接口。那么这种情况只能通过对账这种兜底的方案来解决。对账严格意义上来说不属于分布式事务的解决方案,把它放在这里来讲主要是因为对账也是为了保障分布式系统的数据一致性的。

References

[1] 分布式事务框架seata1.3 AT及XA模式实例演示: https://blog.csdn.net/liubenlong007/article/details/107153355
[2] sagas: https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf