导语:本文参考网络相关文章,主要总结了XA, 2PC, 3PC, 本地事务状态表, 可靠消息队列, 最大努力通知, TCC, SAGA等分布式事务的特点和适用场景,为大家选择分布式事务提供一些参考。
概述
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,相对的,传统事务也称之为单机事务。在单机事务时代,我们通常可以使用数据库的事务操作来解决数据的一致性问题;那么在微服务越来越流行的当下,我们应该如何保证不同服务器上数据的一致性?本文先从CAP理论和BASE理论说起,之后从一致性强弱的角度梳理当前主流的强一致性方案、最终一致性方案和弱一致性方案,最后总结一下各个方案的特点和适用场景,希望对你有所帮助。
预备知识
CAP理论
CAP理论可以说是分布式系统的基石,它说的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项,而不能同时满足。
2000年7月,加州大学伯克利分校的Eric Brewer教授在98年提出CAP猜想,99年发表(Harvest, Yield and Scalable Tolerant Systems),2000年在ACM PODC主题演讲(CAP keynote)。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。
CAP是Consistency、Availability、Partition tolerance三个词的缩写:
- C:一致性,所有客户端看到都是同一份数据,即使在数据更新和删除之后
- A:可用性,即使部分节点发生故障,所有客户端也能找到可用的数据备份
- P:分区容忍性,即使发生网络分区故障,系统仍然能够按照预期正常工作
CAP定理在分布式领域至关重要,在构建大型分布式系统的时候我们必须根据自己业务的独特性在三者之间进行权衡。
由于网络的各种不确定因素,在构建分布式应用的时候我们往往不得不考虑分区容忍性,这个时候我们通常只能在一致性和可用性之间进行选择。
BASE理论
根据CAP定理,如果要完整的实现事务的ACID特性,只能放弃可用性选择一致性,即CP模型。然而如今大多数的互联网应用中,可用性也同样至关重要。于是eBay架构师根据CAP定理进行妥协提出一种ACID替代性方案,即BASE,从而来达到可用性和一致性之间的某种微妙的平衡,选择AP模型的同时最大限度的满足一致性。
由 eBay 架构师 Dan Pritchett 于 2008 年在《BASE: An Acid Alternative》论文中首次提出
BASE是下面三部分的英文缩写简称:
- BA:Basically Available ,基本可用性
- S:Soft State,软状态
- E:Eventually Consistency,最终一致性
“基本可用”是相对CAP的“完全可用”而言的,即在部分节点出现故障的时候不要求整个系统完全可用,允许系统出现部分功能和性能上的损失:比如增加响应时间,引导用户到一个降级提示页面等等。
“软状态”则是相对CAP定理强一致性的“硬状态”而言,CAP定理的一致性要求数据变化要立即反映到所有的节点副本上去,是一种强一致性。“软状态”不要求数据变化立即反映到所有的服务器节点上,允许存在一个中间状态进行过渡,比如允许放大延时等。
“最终一致性”则是相对强一致性而言,它不要求系统数据始终保持一致的状态,只要求系统经过一段时间后最终会达到一致状态即可。
Base 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:强一致性(Strong consistency)无法得到保障时,我们可以根据业务自身的特点,采用适当的方式来达到最终一致性(Eventual consistency)
强一致性方案
强一致性的方案便是前面提到的舍A保C的CP模型,即通过牺牲可用性来保证一致性,这种方案适用于对一致性要求很高的场景,比如金融交易等。
2PC-二阶段提交
二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。
第一个一致性问题实例应该是Lamport的“Time, Clocks and the Ordering of Events in a Distributed System” (1978),大概在这篇论文发表的同一时间,JimGray在“Notes on Database Operating Systems” (1979)中描述了两阶段提交(2PC)。
当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。
因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:
第一阶段:voting phase 投票阶段
事务协调者给每个参与者发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
第二阶段:commit phase 提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
二阶段提交的操作时序图如下:
以上这两个过程被称为“两段式提交”(2 Phase Commit,2PC)协议,而它能够成功保证一致性还需要一些其他前提条件。
- 必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息。两段式提交中投票阶段失败了可以补救(回滚),而提交阶段失败了无法补救(不再改变提交或回滚的结果,只能等崩溃的节点重新恢复),因而此阶段耗时应尽可能短,这也是为了尽量控制网络风险的考虑。
- 必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态。由于在准备阶段已经写入了完整的重做日志,所以当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,并向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作。
两段式提交的原理很简单,也不难实现,但有几个非常明显的缺点:
1. 单点故障问题
协调者在两段提交中具有举足轻重的作用,协调者等待参与者回复时可以有超时机制,允许参与者宕机,但参与者等待协调者指令时无法做超时处理。一旦协调者宕机,所有参与者都会受到影响。如果协调者一直没有恢复,没有正常发送 Commit 或者 Rollback 的指令,那所有参与者都必须一直等待。
2. 同步阻塞问题
执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。也就是说从投票阶段到提交阶段完成这段时间,资源是被锁住的。
3. 数据一致性问题
前面已经提到,两段式提交的成立是有前提条件的,当网络稳定性和宕机恢复能力的假设不成立时,两阶段提交可能会出现一致性问题。
对于宕机恢复能力这一点无需多说。1985 年 Fischer、Lynch、Paterson 用定理(被称为FLP 不可能原理,在分布式中与 CAP 定理齐名)证明了如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确地达成一致性结果。
对于网络稳定性来说,尽管提交阶段时间很短,但仍是明确存在的危险期。如果协调者在发出准备指令后,根据各个参与者发回的信息确定事务状态是可以提交的,协调者就会先持久化事务状态,并提交自己的事务。如果这时候网络忽然断开了,无法再通过网络向所有参与者发出 Commit 指令的话,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既未提交也没办法回滚,导致数据不一致。
3PC-三阶段提交
为了解决两段式提交的单点故障问题、同步阻塞问题和数据一致性问题,“三段式提交”(3 Phase Commit,3PC)协议出现了。
Dale Skeen在“NonBlocking Commit Protocols” (1981)中指出,对于一个分布式系统,需要3阶段的提交算法来避免2PC中的阻塞问题
与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制: 同时在协调者和参与者中都引入超时机制。
- 把原本的2PC的准备阶段再细分为两个阶段: 将准备阶段一分为二的理由是,这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,这时候涉及的数据资源都会被锁住。如果此时某一个参与者无法完成提交,相当于所有的参与者都做了一轮无用功。所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,也意味着因某个参与者提交时发生崩溃而导致全部回滚的风险相对变小了。
三个阶段分别为:
第一阶段:CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
第二阶段:PreCommit阶段
本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有两种:
- 情况1-假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行:
- 情况2-假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
第三阶段:doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
- 情况1-执行提交:针对第一种情况,协调者向各个参与者发起事务提交请求
- 情况2-中断事务:协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
三段式提交的操作时序如下图所示:
可以看出,3PC可以解决单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。
但是3PC对于数据一致性问题并未有任何改进,比如在进入PreCommit阶段后,如果协调者发送的是abort指令,而此时由于网络问题,有部分参与者在等待超时后仍未收到Abort指令的话,那这些参与者就会执行commit,这样就产生了不同参与者之间数据不一致的问题。
由于3PC非常难实现,目前市面上主流的分布式事务解决方案都是2PC协议。
XA协议
2PC 的传统方案是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group 在1994年定义了分布式事务处理模型 DTP(Distributed Transaction Processing Reference Model) 。
DTP 规范中主要包含了 AP、RM、TM 三个部分,如下图所示:
它定义了三大组件:
- AP(Application Program):应用程序,一般指事务的发起者(比如数据库客户端或者访问数据库的程序),定义事务对应的操作(比如更新操作 UPDATE table SET xxx WHERE xxx)
- RMs(Resource Managers):资源管理器,是分布式事务的参与者,管理共享资源,并提供访问接口,供外部程序来访问共享资源,比如数据库、打印服务等,另外 RM 还应该具有事务提交或回滚的能力。
- TM(Transaction Manager):事务管理器,是分布式事务的协调者,管理全局事务,与每个RM进行通信,协调事务的提交和回滚,并协助进行故障恢复。
其中XA约定了TM和RM之间双向通讯的接口规范,并实现了二阶段提交协议,从而在多个数据库资源下保证 ACID 四个特性。所以,DTP模型可以理解为:应用程序访问、使用RM的资源,并通过TM的事务接口(TX interface)定义需要执行的事务操作,然后TM和RM会基于 XA 规范,执行二阶段提交协议进行事务的提交/回滚:
- 准备阶段:TM 向所有与当前事务相关的 RM 发起准备请求,每个 RM 评估自身是否能正常进行提交,并在响应中告知 TM。这类似于一个 TM 发起投票,各个 RM 进行投票的过程。
- 提交阶段:在第一阶段中,如果所有 RM 能在一定时间内表示 OK,则第二阶段由 TM 向所有 RM 发起提交请求。有任何一个 RM 没有准备好,TM 则向所有 RM 发起回滚请求。
目前大多数实现XA的都是一些关系型数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息中间件(包括ActiveMQ,HornetQ,MSMQ和IBM MQ),所以提起XA往往指基于资源层的底层分布式事务解决方案。
MySQL 从5.0.3开始支持XA分布式事务(只有InnoDB引擎才支持XA事务),业务开发人员在编写代码时,不应该直接操作这些XA事务操作的接口。因为在DTP模型中,RM上的事务分支的开启、结束、准备、提交、回滚等操作,都应该是由事务管理器TM来统一管理。
XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。基于两阶段提交的分布式事务在提交事务时需要在多个节点之间进行协调,最大限度延后了提交事务的时间点,客观上延长了事务的执行时间,这会导致事务在访问共享资源时发生冲突和死锁的概率增高。因此,XA并发性能不理想(使用 XA 协议的 MySQL 集群,操作延时是单机的 10 倍),无法满足高并发场景,在互联网中使用较少。
最终一致性方案
上面所述的强一致性方案在性能上都不理想,在CAP定理中属于CP范畴;在互联网应用中,为了提升性能和可用性,基于BASE理论使用最终一致性来替代强一致性,也就是通过牺牲部分一致性来换取性能和可用性的提升。
本地事务状态表
本地事务状态表方案的大概处理流程是:
- 在调用方请求外部系统前将待执行的事务流程及其状态信息存储到数据库中,依赖数据库本地事务的原子特性保证本地事务和调用外部系统事务的一致性,这个存储事务执行状态信息的表称为本地事务状态表。
- 在将事务状态信息存储到DB后,调用方才会开始继续后面流程,同步调用外部系统,并且每次调用成功后会更新相应的子事务状态,某一步失败时则中止执行。
- 同时在后台运行一个定时任务,定期扫描事务状态表中未完成的子事务,并重新发起调用,或者执行回滚,或者在失败重试指定次数后触发告警让人工介入进行修复。
如下图所示:
其中本地事务表的设计由业务方自己来定,可以是如上图中所示拆分为多个子事务来管理,简单点也可以只有一条记录,然后通过状态的流转来控制程序调用不同的外部系统。
可靠消息队列
可靠消息队列方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收到消息并处理事务成功,此方案强调的是只要消息发给事务参与方,则最终事务要达到一致。
此方案是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。
因此可靠消息最终一致性方案要解决以下几个问题:
- 本地事务与消息发送的原子性问题:要求事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息
- 事务参与方接收消息的可靠性:要求事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息
- 消息重复消费的问题:要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
目前主要的解决方案有2种,一种是本地消息表方案,一种是事务消息方案。
本地消息表
如果是使用 Kafka(< 0.11.0) 这类不支持事务消息的消息中间件,参与事务的系统需要在给消息中间件发送消息之前,把消息的信息和状态存储到本地的消息表中,如下图所示:
资料领取直通车:大厂面试题锦集+视频教程https://docs.qq.com/doc/DTlhVekRrZUdDUEpy
Linux服务器学习网站:C/C++Linux服务器开发/后台架构师https://ke.qq.com/course/417774?flowToken=1028592
主要流程如下:
- 参与分布式事务的系统A接收到请求后,在执行本地事务的同时把将待发送的消息记录到事务消息表中,将业务表和消息表放在一个数据库事务里,保证两者的原子性;
- 执行完后系统A不直接给消息中间件发消息,而是通过后台定时任务扫描消息表将消息push到消息中间件,对于push失败的消息则会不断重试,直到消息中间件成功返回 ack 消息,并更细消息表中投递状态,从而保证消息的不丢失。
- 消息中间件收到消息后,会将消息投递给订阅消息的外部系统1/2/3,外部系统1/2/3收到消息后执行本地事务,只有成功才应答 Ack 消息,消息中间件只有在收到Ack消息后才将该条消息丢弃,否则会不断的重复发送直到成功,所以事务的所有参与者需要自行保证事务执行的幂等性。
事务消息方案
如果是基于RocketMQ或Kafka(>=0.11.0)这类的支持事务操作的消息中间件,上述的方案则可以简化,此时上面的的定时任务的工作将交给消息中间件来提供流程比较简单不再赘述,需要说明的是,消息中间件如果收到Comfirm消息,则会将消息转为对消费者可见,并开始投递;如果收到Rollback消息,则会删除之前的事务消息;如果未收到确认消息,则会通过事务回查机制定时检查本地事务的状态,决定是否可以提交投递。
最大努力通知
最大努力通知方案( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。最大努力通知型的实现方案,一般符合以下特点:
- 不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
- 定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。
比如充值的一个例子:
与可靠消息队列方案区别:
- 解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
- 两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
- 技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
TCC
前面介绍的可靠消息队列方案能保证最终的结果是相对可靠的,过程也足够简单,但是可靠消息队列的整个实现过程完全没有任何隔离性可言。虽然在有些业务中,有没有隔离性不是很重要,比如说搜索系统。
但在有些业务中,一旦缺乏了隔离性,就会带来许多麻烦,比如下面一个简化版的订销存交易流程:
用户在电商网站下订单后通知库存服务扣减粗存,最后通过积分服务给用户增加积分。整个交易操作应该具有原子性,这些交易步骤要么一起成功,要么一起失败,必须是一个整体性的事务。
假设用户下完订单通知库存服务扣减库存失败时,比如原本是10件商品卖了1件剩余9件,但由于库存DB操作失败,导致库存还是10件,这时就出现了数据不一致的情况,此时如果有其它用户也进行了购买操作,则可能出现超卖的问题。
如果采用2PC的解决方案,在整个交易成功完成或者失败回滚之前,其它用户的操作将会处于阻塞等待的状态,这会大大的降低系统的性能和用户体验。
如果业务需要隔离,通常就应该重点考虑 TCC(Try-Confirm-Cancel)方案,TCC天生适用于需要强隔离性的分布式事务中,它是由数据库专家帕特 · 赫兰德(Pat Helland)在 2007 年撰写的论文《Life beyond Distributed Transactions: An Apostate’s Opinion》中提出的。
在具体实现上,TCC 的操作其实有点儿麻烦和复杂,它是一种业务侵入性较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认 / 释放消费资源”两个子过程。另外,你看名字也能看出来,TCC 的实现过程分为了三个阶段:
- Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(保障隔离性)。
- Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。注意,Confirm 阶段可能会重复执行,因此需要满足幂等性。
- Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。注意,Cancel 阶段也可能会重复执行,因此也需要满足幂等性。
TCC是基于BASE理论的类2PC方案,根据业务的特性对2PC的流程进行了优化,与2PC的区别在一些步骤的细节上,如下图:
可以看出,不同于2PC第一阶段的Prepare,TCC在Try阶段主要是对资源的预留操作这类的轻量级操作,比如冻结部分库存数量,它不需要像2PC在第二阶段完成之后才释放整个资源,也就是它不需要等待整个事务完成后才进行提交,这时其它用户的购买操作可以继续正常进行,因此它的阻塞范围小时间短暂,性能上比2PC方案要有很大的提升。
另外,TCC是位于用户代码层面,而不是在基础设施层面,这为它的实现带来了较高的灵活性,可以根据需要设计资源锁定的粒度。TCC 在业务执行时只操作预留资源,几乎不会涉及锁和资源的争用,具有很高的性能潜力。
但是 TCC要求所有的事务参与方都必须要提供三个操作接口:Try/Confirm/Cancel,带来了更高的开发成本和业务侵入性,意味着有更高的开发成本和更换事务实现方案的替换成本,特别是对一些难以改动的老旧系统来说甚至是不可行的。
SAGA事务
SAGA 事务模式的历史十分悠久,比分布式事务的概念提出还要更早。SAGA 的意思是“长篇故事、长篇记叙、一长串事件”,它起源于 1987 年普林斯顿大学的赫克托 · 加西亚 · 莫利纳(Hector Garcia Molina)和肯尼斯 · 麦克米伦(Kenneth Salem)在 ACM 发表的一篇论文《SAGAS》。
文中提出了一种如何提升“长时间事务”(Long Lived Transaction)运作效率的方法,大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出 SAGA 的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。
Saga 事务基本协议如下:
- 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) T1,T2,…,Ti,…,Tn组成。
- 每个 Ti 都有对应的幂等补偿动作C1,C2,…,Ci,…,Cn,补偿动作用于撤销 T1,T2,…,Ti,…,Tn造成的结果。
如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成。否则,就要采取恢复策略,恢复策略分为向前恢复和向后恢复两种。
向前恢复(Forward Recovery)
如果 Ti 事务提交失败,则一直对 Ti 进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,比如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn,该情况下不需要Ci。
向后恢复(Backward Recovery)
如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直至成功为止(最大努力交付)。这里要求 Ci 必须(在持续重试后)执行成功。向后恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
Saga 事务常见的有两种不同的实现方式。
命令协调模式
这种模式由中央协调器(Orchestrator,简称 OSO)集中处理事件的决策和业务逻辑排序,以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
- 事务发起方的主业务逻辑请求 OSO 服务开启订单事务。
- OSO 向库存服务请求扣减库存,库存服务回复处理结果。
- OSO 向订单服务请求创建订单,订单服务回复创建结果。
- OSO 向支付服务请求支付,支付服务回复处理结果。
- 主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排模式
这种模式没有中央协调器(没有单点风险),由每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
电商订单的例子为例:
- 事务发起方的主业务逻辑发布开始订单事件。
- 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
- 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
- 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
- 主业务逻辑监听订单已支付事件并处理。
事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
SAGA的适用场景主要是以下几种:
- 业务流程长、业务流程多
- 参与者包含第三方或遗留系统服务,无法提供TCC模式要求的三个接口
- 典型业务系统:如金融网络(与外部金融机构对接)、互联网微贷、渠道整合、分布式架构服务集成等业务系统
- 银行业金融机构使用广泛
SAGA优势主要体现在:
- 一阶段提交本地数据库事务,无锁,高性能;
- 参与者可以采用事务驱动异步执行,高吞吐;
- 补偿服务即正向服务的“反向”,易于理解,易于实现;
但是Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。
弱一致性方案
前面最终一致性方案基本能满足大多数的场景,但在一些场景下,我们对系统的性能和可用性有更高的要求。比如海量请求的高并发秒杀场景中,如何保证服务的高可用是个很大的挑战,除了要对秒杀的非核心功能进行降级、增加响应时间外,根据CAP定理,还需要对对一致性的再进行妥协,从最终一致性弱化到弱一致性。
弱一致性是指数据更新后,容忍后续只能访问到部分或者全部访问不到(也不承诺多久可以访问到),并且不会对业务产生重大影响。下面介绍的几个方案都是根据自身业务特点做的妥协,不是严格意义上完备的技术方案,而是一种解决思路,是适合业务自身特点、满足性能要求、满足成本要求或技术架构要求下的一种解决思路,仅供参考。
基于状态的补偿
这是一个根据业务特性进行妥协的一种方案,根据实际的业务场景对立面的数据重要性进行划分,放弃传统的全局数据一致,允许部分不重要的数据出现不一致,但不会对业务产生重大影响。
比如在电商网站购物场景中,其中两个主要的步骤是创建订单和扣库存,这分别由两个服务进行处理:订单服务和库存服务。
- 如果采用前面可靠消息队列方案,创建订单的消息通知库存服务扣除库存,由于异步消息的延迟则会导致超卖;
- 如果采用TCC的方案,每次请求操作都需要Try、Confirm两次请求调用,性能又不能达标;
- 如果采用本地事务状态表,则需要对海量的事务进行状态更新操作,性能和延迟也同样会是个问题。
但是我们可以依据实际的电商购物场景进行取舍:允许少卖,但不能超卖。于是我们可以先扣库存,库存扣减成功后才创建订单并关联库存,若扣库存失败则不创建订单。有以下几种情况:
扣库存 | 创建订单 | 返回结果 | 可能结果 | |
1 | √ | √ | √ | 正常 |
2 | √ | × | × | 多扣库存,少卖 |
3 | × | × | × | 下单失败 |
对于第2种情况,会出现多扣库存的情况,这时可以基于状态进行补偿,就不会出现超卖的问题了:根据库存流水记录查找那些一段时间内未关联订单的库存记录进行撤销操作。这个和我们在12306上的买车票,如果30分钟内未支付的话车票会被释放,是一个道理。
这是一种事后处理机制,即使补偿失败,也不会有严重后果,对业务来说也是可接受的,大不了手工重新上架。
事后对账
对于那些业务流程复杂,涉及外部服务比较多,并且需要维护的状态也很复杂的场景,就很难根据状态进行自动补偿,这时可以进一步简化操作:不做自动的状态补偿。
还是拿上面那个订单和库存的例子进行说明,比如先扣库存,然后创建订单,如果订单创建失败则重试,重试还是失败则回滚,回滚失败则触发告警,然后由脚本对业务数据自动进行对账,并对异常数据进行修复。
对账的关键是找出数据的特征,有些好找,有些难,但是它的基本要求是数据记录是“完备”的,然后研发人员根据经验,对不同特征的数据执行不同的修复,对于常见的问题可能会有一些修复脚本来辅助处理。
事后对账一般会根据业务特点设计自动对账脚本,实现对业务数据的自动检查,发现业务中可能存在的问题(比如异常、假账)等,然后触发执行对应的动作,至少是要有告警,通知研发同学介入,如果做的更好一点的话,可以对特定类型的异常数据自动进行修复,减少人工成本。
总结
强一致性方案主要用于对数据一致性要求比较高的场景,比如金融银行等,且大多是在数据库层面实现,然后业务方直接使用;
在常规的互联网应用中,对性能和可用性要求更高,可以采用基于BASE理论的最终一致性方案;
弱一致性方案需要容忍数据的部分不一致,主要用于一些极端的场景中,比如高并发秒杀场景。
各个方案的特点总结如下:
- 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,不适合高并发和高性能要求的场景。
- XA协议:基于XA协议的强一致事务使用起来相对简单,但是无法很好地应对互联网的短事务和高并发场景。
- 本地事务状态表:方案轻量,容易实现,但与具体的业务场景耦合较高,不可公用。
- 可靠消息队列:适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。
- 最大努力通知:是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等。
- TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。但是对于业务的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
- SAGA:适合于“业务流程长、业务流程多”的场景。特别是针对参与事务的服务是遗留系统服务。但由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。另外, Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。
- 弱一致性方案:上面给出的几种弱一致性方案是在高并发等场景下,为了提高系统的性能和可用性而在一致性方面做的妥协,一般需要结合具体的业务特点、实现成本等各方因素对一些最终一致性方案做改造。
总之,分布式事务没有能一揽子包治百病的解决方案,只有因地制宜地选用适合自己的,才是唯一有效的做法。
以下是在网上看到的一些技术大佬的经验之谈,共勉之:
- 实际运用理论时进行架构设计时,许多人容易犯“手里有了锤子,看什么都觉得像钉子”的错误,设计方案时考虑的问题场景过多,各种重试,各种补偿机制引入系统,导致系统过于复杂,落地遥遥无期。世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!
- 有些问题,看起来很重要,但实际上我们可以通过合理的设计或者将问题分解来规避。
- 设计分布式事务系统也不是需要考虑所有异常情况,不必过度设计各种回滚,补偿机制。
- 如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。
- 如果系统要实现回滚流程的话,有可能系统复杂度将大大提升,且很容易出现 Bug,估计出现 Bug 的概率会比需要事务回滚的概率大很多。
- 在设计系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,可以考虑当出现这个概率很小的问题,能否采用人工解决的方式,这也是大家在解决疑难问题时需要多多思考的地方。
开源框架
目前我所了解的分布式事务解决方案的开源框架主要是Seata和Dtm,但都未实际使用过,所以没有发言权,在此只是列出来各自框架的主要特点,仅供参考。
Seata
Seata(Simple Extensible Autonomous Transaction Architecture,一站式分布式事务解决方案)是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案(https://github.com/seata/seata)。目前start数有21k。
如下图所示,Seata 中有三大模块,分别是 TM、RM 和 TC。 其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。
在 Seata 中,分布式事务的执行流程:
- TM 开启分布式事务(TM 向 TC 注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
- TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;
Seata 会有 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。
DTM
DTM是一款golang开发的分布式事务管理器,目前star数4.6k,它解决了跨数据库、跨服务、跨语言栈更新数据的一致性问题。他优雅的解决了幂等、空补偿、悬挂等分布式事务难题,提供了简单易用、高性能、易水平扩展的解决方案。
亮点如下:
- 极易接入:零配置启动服务,提供非常简单的HTTP接口,极大降低上手分布式事务的难度,新手也能快速接入
- 跨语言:可适合多语言栈的公司使用。方便go、python、php、nodejs、ruby、c# 各类语言使用。
- 使用简单:开发者不再担心悬挂、空补偿、幂等各类问题,首创子事务屏障技术代为处理
- 易部署、易扩展:依赖mysql|redis,部署简单,易集群化,易水平扩展
- 多种分布式事务协议支持:TCC、SAGA、XA、二阶段消息,一站式解决所有分布式事务问题
与其他框架对比(非Java语言类的,暂未看到除dtm之外的成熟框架,因此这里将DTM和Java中最成熟的Seata对比):