编辑推荐: |
分布式系统由于机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失等原因面临一系列挑战,本文重点讲述分布式系统面临的挑战之一数据一致性问题。
本文来自于微信公众号腾讯技术工程 ,由火龙果软件Linda编辑推荐。 |
|
随着计算能力的提升、互联网的兴起、数据的分布和存储需求、容错性和可用性的要求、业务的分布和协同需求以及云计算和大数据技术的发展,分布式系统变得越来越重要,并在各个领域得到广泛应用。分布式系统由于机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的
TCP、存储数据丢失等原因面临一系列挑战,本文重点讲述分布式系统面临的挑战之一数据一致性问题。
本地事务
ACID:数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性(
Isolation)和持久性(Durabilily)
● 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
● 一致性:事务在开始前和结束后,数据库的完整性约束没有被破坏
● 隔离性:事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据
● 持久性:一个事务完成之后,事务的执行结果必须是落盘在数据库持久
分布式基本理论
1、CAP 定理
CAP是指一致性(Consistency)、可用性(Availability)和分区容忍性(Partition
Tolerance)三个属性,它们是分布式系统设计中的重要概念。
● 一致性(Consistency):
在分布式系统中的所有数据结点,在同一时刻是否同样的值。如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致;
● 可用性(Availability):
在集群中一部分节点故障后,非故障的节点在合理的时间内返回合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回;合理的响应指的是系统应该明确返回结果并且结果是正确的;
● 分区容错性(Partition tolerance):
系统能够在节点之间发生网络分区(Partition)的情况下仍然能够正常运行;
在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。
对于CP来说,放弃可用性,追求一致性(强一致性)和分区容错性;对于AP来说,放弃一致性,追求分区容错性和可用性,这是很多分布式系统设计时的选择,BASE是根据AP来扩展。在实际应用中,网络延迟和不可靠性是不可避免的,数据复制和同步需要一定的时间。因此,即使选择了保证一致性和分区容忍性(CP),在发生网络分区时,节点之间的数据复制可能会产生一定的延迟,导致节点之间的数据不一致,所以很多业务场景我们退而求用户能接受时间延迟的最终一致方案。
2、BASE理论
根据CAP定理,如果要完整的实现事务的ACID特性,只能放弃可用性选择一致性,然而可用性在现在互联网环境至关重要,BASE
理论是对 CAP 中一致性和可用性权衡的结果,是CAP中AP的一个扩展。其核心思想是:强一致性无法得到保障时,我们可以根据业务自身的特点,采用适当的方式来达到最终一致性。BASE
是 Basically Available(基本可用)、Soft state(软状态)和 Eventually
consistent (最终一致性)三个短语的缩写。
● BA:(Basically Available)基本可用性,分布式系统在面对故障或分区的情况下,仍然能够保证基本的可用性。即系统可以继续运行并提供核心的功能,而不是完全崩溃;
● S:(Soft State)软状态,分布式系统中的数据状态不需要实时保持一致,而是允许一段时间的数据不一致。数据状态可以是中间状态,可以根据系统自身的需要而变化,这种状态允许一定的延迟和不一致性;
● E:(Eventually Consistency)最终一致性,经过一段时间后数据最终会达到一致状态,但不要求实时的一致性。
分布式事务方案
1、2PC
2PC(Two-Phase Commit)是一种分布式系统中常用的协议,用于实现多个参与者之间的分布式事务的一致性。它包括两个阶段的操作:准备阶段(Prepare
Phase)和提交阶段(Commit Phase)。
以下是2PC协议的基本工作流程:
● 准备阶段(Prepare Phase):
○ 协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应;
○ 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志;
○ 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个YES响应;如果参与者节点的事务操作实际执行失败,则它返回一个NO响应;
● 提交阶段(Commit Phase):
成功:当协调者节点从所有参与者节点获得的响应都为YES时:
○ 协调者节点向所有参与者节点发出Commit的请求;
○ 参与者节点正式完成操作,并释放在整个事务期间内占用的资源,参与者节点向协调者节点发送Committed消息;
○ 协调者节点收到所有参与者节点反馈的Committed消息后,完成事务;
失败:如果任一参与者节点在第一阶段返回的响应消息为"终止",或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
○ 协调者节点向所有参与者节点发出Rollback的请求;
○ 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源,参与者节点向协调者节点发送Rollbacked消息;
○ 协调者节点收到所有参与者节点反馈的Rollbacked消息后,取消事务;
优点:
● 简单:尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现;
缺点:
● 单点问题:协调者在两段提交中具有举足轻重的作用,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作;
● 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源,这样的过程会比较漫长,对性能影响比较大;
● 数据不一致:当网络不稳定性和宕机恢复有问题时,会存在数据不一致性的可能,虽然可以通过;
2、3PC
三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。与两阶段提交不同的是,三阶段提交有两个改动点,有效解决长时间阻塞和协调者单点故障:
\1) 引入超时机制:同时在协调者和参与者中都引入超时机制;
\2) 把2PC的准备阶段再细分为两个阶段: 将准备阶段一分为二的理由是,这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,这时候涉及的数据资源都会被锁住。如果此时某一个参与者无法完成提交,相当于所有的参与者都做了一轮无用功。所以,增加一轮询问阶段,相比来说更加轻量。这个阶段参与者并不真实获取锁占用资源,只是对自身执行事务状态的检查,查看是否具备执行事务的条件。
3PC协议的基本工作流程:
● 准备阶段(Prepare Phase):
○ 协调者向所有参与者发送准备请求,并等待参与者的响应;
○ 参与者接收到准备请求后,执行本地事务操作,并将准备就绪状态(Prepare Ready)或中止状态(Abort)的响应发送给协调者;
● 预提交阶段(Precommit Phase):
○ 协调者根据参与者的响应情况,判断是否所有参与者都准备就绪;
○ 如果所有参与者都准备就绪,协调者向所有参与者发送预提交请求,并等待参与者的响应;
○ 参与者接收到预提交请求后,执行事务的预提交操作,并将预提交完成状态(Precommit)或中止状态(Abort)的响应发送给协调者;
● 提交阶段(Commit Phase):
○ 协调者根据参与者的响应情况,判断是否所有参与者都预提交成功;
○ 如果所有参与者都预提交成功,协调者向所有参与者发送提交请求,并等待参与者的响应;
○ 参与者接收到提交请求后,执行事务的最终提交操作,并将提交完成状态(Commit)或中止状态(Abort)的响应发送给协调者;
优点:3PC可以解决单点故障问题,并减少阻塞。一旦参与者无法及时收到来自协调者的信息之后,会默认执行commit,而不会一直持有事务资源并处于阻塞状态。
缺点:一致性问题照样存在,比如在进入PreCommit阶段后,如果协调者发送的是abort指令,而此时由于网络问题,有部分参与者在等待超时后仍未收到Abort指令的话,那这些参与者就会执行commit,这样就产生了不同参与者之间数据不一致的问题。
3、XA
XA(eXtended Architecture)是一种分布式事务处理的标准协议,用于确保多个资源管理器(Resource
Manager)之间的事务一致性。它提供了在分布式环境中同时提交或回滚多个资源的机制。
目前一些关系型数据库和消息队列有支持XA协议,XA往往指基于资源层的底层分布式事务解决方案。分布式事务处理(Distributed
Transaction Processing,DTP)模型定义了一个标准化的分布式事务处理的体系结构以及交互接口,DTP
规范中主要包含了 AP、RM、TM 三个部分,如下图所示:
组件概念:
● AP:(application program)应用程序:事务的发起者,指定了构成全局事务的相关数据访问操作;
● RM:(resource manager)资源管理器:事务的参与者,管理事务处理过程中涉及到的各种资源,如数据库、消息队列等,当发生故障后,资源管理器可以将数据资源恢复到一致状态;
● TM:(transaction manager)事务管理器,事务的协调者,负责管理分布式事务的整个生命周期,包括事务的提交、回滚和恢复等;
XA约定了TM和RM之间双向通讯的接口规范,并实现了二阶段提交协议,从而在多个数据库资源下保证 ACID
四个特性。所以,DTP模型可以理解为:应用程序访问、使用RM的资源,并通过TM的事务接口(TX interface)定义需要执行的事务操作,然后TM和RM会基于
XA 规范,执行二阶段提交协议进行事务的提交/回滚:
● 事务准备阶段(Transaction Prepare Phase):
TM向所有RM发送事务开始请求,并开始全局事务。RM接收到事务开始请求后,执行本地事务操作,并将事务的执行状态通知TM;。
● 事务提交/回滚阶段(Transaction Commit/Rollback Phase):
如果所有RM的事务都成功执行,TM向所有RM发送事务提交请求。RM接收到事务提交请求后,将事务结果持久化,并通知TM提交完成;如果任何一个RM的事务执行失败,TM向所有RM发送事务回滚请求。RM接收到事务回滚请求后,将事务回滚,并通知TM回滚完成;
优点:
● 简单易理解;
● 开发较容易,回滚之类的操作,由底层数据库自动完成;
缺点:
● 需要资源支持XA协议,非关系型数据库大多不支持;
● 基于两阶段提交协议在提交事务时需要在多个节点之间进行协调,最大限度延后了提交事务的时间点,客观上延长了事务的执行时间,这会导致事务在访问共享资源时发生冲突和死锁的概率增高,并发度低,不适合高并发的业务;
● 基于两阶段提交协议进行事务,由于网络故障和参与者故障的存在,XA事务的一致性可能在有限时间窗口内实现,并且在故障发生时可能存在一段时间的不一致状态。因此,在设计和使用XA事务时,需要考虑系统的可用性和容错性,并合理权衡一致性的需求;
4、TCC
TCC(Try-Confirm-Cancel)是一种补偿性事务模式,它通过明确的三个阶段来保证分布式事务的一致性和可靠性。TCC模式适用于需要跨多个服务进行分布式事务处理的场景。
TCC模式将一个分布式事务拆分为三个阶段:
\1) Try:(尝试阶段)在Try阶段,事务发起方资源检查和预留,预留好事务需要用到的所有业务资源;Try
阶段可能会重复执行,因此需要满足幂等性,同时需要需要支持防悬挂控制,比如Try超时,Cancel先到,Try后到场景,需要Cancel记录事务id,Try对于该id拒绝执行;
\2) Confirm:(确认阶段):如果所有参与者在Try阶段都执行成功,事务发起方会发送确认请求,要求各个参与者执行真正的提交操作;Confirm
阶段可能会重复执行,因此需要满足幂等性;
\3) Cancel:(取消阶段):如果任何一个参与者在Try阶段执行失败或者Confirm阶段执行失败,事务发起方会发送取消请求,要求各个参与者执行回滚操作,撤销Try阶段的操作。Cancel
阶段可能会重复执行,因此需要满足幂等性;同时允许空回滚,比如Try消息丢失,需要Cancel请求时返回成功;
优点:
● 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源;
● 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性;
● 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群;
缺点:
● 有代码侵入,TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本;
● 软状态,事务是最终一致;
● 需要考虑Confirm和Cancel的失败情况,做好幂等处理;
5、SAGA
Saga是一种用于处理分布式事务的协调模式,旨在提供一致性保证并支持长时间执行的事务。Saga模式将一个大型事务拆分为多个小的、离散的事务片段,每个片段都具有自己的本地事务和补偿操作。Saga模式的关键特点包括:
● 分布式事务拆分:Saga将大型事务拆分为多个小的事务片段,每个片段都可以独立执行,并具有自己的本地事务;
● 补偿操作:如果某个事务片段失败,Saga会触发相应的补偿操作,以回滚或撤销已执行的操作,以维持整个事务的一致性。
Saga每个片段都会执行一些操作,如果所有片段都成功完成,则事务被提交。如果某个片段失败,则会触发相应的补偿操作,恢复策略分为向前恢复和向后恢复两种,以保持整个事务的一致性。
\1) 向前恢复(forward recovery):
如果 Ti 事务提交失败,则一直对 Ti 及Ti之后的进行重试,直至成功为止。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,如上面的图例,子事务按照从左到右的顺序执行,T1执行完毕以后T2
执行,然后是T3、T4、T5;
\2) 向后恢复(backward recovery):
如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直至成功为止(最大努力交付)。这里要求
Ci 必须(在持续重试后)执行成功。向后恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1;
通常来说我们把这种Saga执行事务的顺序称为个Saga的协调逻辑。这种协调逻辑有两种模式,编排(Choreography)和控制(Orchestration)分别如下:
\1) 编排(Choreography):参与者(子事务)之间的调用、分配、决策和排序,通过交换事件进行进行。是一种去中心化的模式,参与者之间通过消息机制进行沟通,通过监听器的方式监听其他参与者发出的消息,从而执行后续的逻辑处理。由于没有中间协调点,靠参与靠自己进行相互协调;
优点:
● 灵活性:可以提供灵活的事务流程控制,每个片段可以依据业务自定义自己的命令消息进行对应的操作,能够适应各种复杂的业务流程;
● 可扩展性:通过使用命令消息来控制事务片段的执行和补偿操作,可以轻松地扩展和修改事务流程,适应不断变化的业务需求;
缺点:
● 复杂性:需要开发人员设计和管理命令消息的传递和处理逻辑。这增加了系统的复杂性,需要确保命令消息的正确传递、处理和顺序执行。错误的命令消息处理可能导致事务的不一致或错误的补偿操作;
● 消息传递开销:使用异步消息传递来传递命令消息,并在事务片段之间进行协调。这会引入一定的消息传递开销,包括消息的发送、传输、接收和处理。如果系统中存在大量的事务片段和复杂的事务流程,消息传递的开销可能会增加;
\2) 控制(Orchestration):Saga提供一个控制类,其方便参与者之前的协调工作。事务执行的命令从控制类发起,按照逻辑顺序请求Saga的参与者,从参与者那里接受到反馈以后,控制类在发起向其他参与者的调用。所有Saga的参与者都围绕这个控制类进行沟通和协调工作;
优点:
● 集中控制: 通过saga协调器来集中控制事务片段的执行和补偿操作,使得系统的事务流程更加可控和可管理;
● 易于理解和维护:将事务片段的执行和补偿操作集中在一个地方,使得系统的事务流程更加清晰和易于理解。开发人员可以更轻松地查看和维护事务的执行逻辑,减少了分散的代码和逻辑;
缺点:
● 单点故障:saga协调器是整个事务流程的关键组件,如果它发生故障或成为系统的瓶颈,将影响整个系统的可用性和性能。单点故障可能导致事务的中断、延迟或失败;
● 性能开销: 引入了一个saga协调器来控制事务片段的执行和补偿操作,会增加系统的性能开销,包括协调器的处理能力和网络通信的开销;
● 复杂性: 将事务的执行和补偿操作集中在一个地方,使得协调器的设计和实现变得复杂。开发人员需要考虑事务片段的顺序、补偿操作的触发条件和错误处理机制等方面,增加了系统的复杂性;
Saga模式的优势在于它能够处理长时间执行的事务,并允许部分提交和部分回滚。它可以适应分布式系统中的故障和部分失败,并通过补偿操作来保持一致性。然而,Saga模式也增加了系统的复杂性,因为开发人员需要设计和实现每个事务片段的逻辑和补偿操作,并确保在故障或中断情况下的正确执行。
TCC协议与Saga模式有一些相似之处。两者都是将大型事务拆分为多个小事务,并支持补偿操作。然而,TCC协议更加关注于资源锁粒度的控制,而Saga模式更加注重长时间执行事务的处理和补偿机制。
6、本地消息表
本地消息表事务(Local Message Table Transaction)是一种可靠消息事务机制,核心思想就是将分布式事务拆分成本地事务进行处理,在该方案中主要有两种角色:事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
基本思路就是:
\1) 事务主动方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,可进行重试发送或通过定时任务发送到MQ;
\2) 事务被动方,需要消费消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的成功,可以给事务主动方发送一个业务消息,通知事务主动方标记或删除事务消息,需要可重入。
优点:
● 实现逻辑简单,开发成本低;
● 比较好的容错性,弱化了对MQ组件的特性依赖;
缺点:
● 事务消息与业务强耦合,不可公用;
● 本地消息表基于数据库支持,事务消息占用业务IO资源,高并发场景有性能局限;
7、MQ事务消息
MQ事务消息是一种可靠消息事务机制,是一种在消息队列(Message Queue,MQ)系统中实现分布式事务的机制。它结合了事务处理和消息队列的概念,用于确保多个消息的原子性和一致性。基于
MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
通过事务消息实现分布式事务的流程如下:
\1) 发起方发送半事务消息会给消息队列 ,此时消息的状态prepare,接受方还不能拉取到此消息
\2) 发起方进行本地事务操作
\3) 发起方给消息队列确认提交消息,此时接受方可以消费到此消息了
优点:
● 消息数据独立存储,降低业务系统与消息系统之间的耦合;
● 吞吐量优于本地消息表方案;
缺点:
● 一次消息发送需要两次网络请求(half消息 + commit/rollback);
● 发起方需要实现消息回查接口;
8、最大努力通知
最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果
不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。最大努力通知型的实现方案,一般符合以下特点:
● 不可靠消息:业务主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次(时间退避)后不再通知,允许消息丢失(不可靠消息);
● 定期校对:业务被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息;
最大努力通知与可靠消息最终一致性的区别:
● 解决方案思想不同
○ 可靠消息:发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证;
○ 最大努力通知:发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方;
● 业务场景不同
○ 可靠消息:关注的是交易过程的事务一致,以异步的方式完成交易;
○ 最大努力通知:关注的是交易后的通知事务,即将交易结果可靠的通知出去;
● 技术解决方向不同
○ 可靠消息:要解决消息从发出到接收的一致性,即消息发出并且被接收到;
○ 最大努力通知:无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果);
综合对比
项目实战方案
1、本地共享内存机制
本地共享内存机制借鉴本地消息表和存储操作日志原理,将事件信息写入共享内存,共享内存使用环状队列管理事件信息。将事务拆分为关键事务和一系列子事务,事务主动方负责写共享内存和执行关键事务,通过监测服务异步推送子事务执行,并通过状态机保障最终所有子事务按照预期顺序执行。
核心执行流程:
\1. 事务主动方收到请求后先将事件写入共享内存队列,内存写入成功后执行关键事务;
\2. 监测服务出队事件消息,查询关键事务存储数据,确定是否执行成功,如果失败直接放弃该事件消息;
\3. 按照预期子事务顺序执行子事务,如果遇到子事务执行失败,标记执行进度重新写入共享内存队列;至到所有子事务执行完成;
优点:
● 实现简单:本地共享内存写事件消息与关键事务存储隔离,子事务流程集中管理方便;
● 扩展性强:事务扩展能力强;
● 松耦合:所有子事务没有强关联,很符合微服务架构需要;
缺点:
● 单点:事件消息单机存储,遇到单点问题时可能会丢消息;
● 性能差:事务主动方与监测服务强关联,资源共享会争抢资源,性能在高并发场景有局限;同时为了保障顺序执行,无法并行执行,子事务比较多情况吞吐量上不去;
● 丢消息:数据读取后,遇到异常退出事件信息未及时写入内存会丢失改事件;
● 时序无保障:事件消息随单机存储,在多节点部署场景,无法保障时序;
2、检测存储binlog/oplog
不少存储通过记录数据库中的写操作到日志,以支持数据复制、故障恢复和数据同步等功能。同时也支持操作日志对外watch的能力,供业务使用。比如mysql
的binlog,mongodb的oplog,oracle的redo log等,通过这些存储提供的相应watch能力,把相关数据转存到MQ,异步进行事务操作,下面列举部分存储:
● mysql:MySQL 提供了 Binlog API,通过该 API 订阅和消费 binlog
事件;
● oracle:OEM 提供了一组 RESTful API 接口,可以用于监控和管理 Oracle
数据库。这些 API 可以用于查询和操作数据库的各种日志信息,包括 Redo Log、归档日志等;
● mongodb:oplog,通过 Change Streams,可以订阅 MongoDB 集合的变更事件并监听事件;
使用存储操作日志,事务主动方只需要关注写入存储就能做到存储与MQ消息一致,事务被动方消费消息自己保障业务,两者完全解耦,并具备比较好的业务扩展能力。监测服务保障操作事件至少一次到达MQ组件,事务被动方需要提供可重入能力;同时监测服务需要规避单机问题,需要引入合适的分布式协调策略完成保障一次事件尽量只一次同步到MQ,以下是一些常用的方式:
● 分布式锁:通过分布式锁,保障当前只有一个存储事件只有一个监测服务写入到MQ组件,需要解决锁释放、死锁、脑裂等问题。可以使用乐观锁和悲观锁等,也可以用成熟的锁组件,如redis、zookeeper、etcd;
● 共识选举算法:使用共识算法,保障只有一个服务能watch对应日志事件;如raft、paxos
优点:
● 逻辑简单:事务主动方只需要写存储,不需要关注事务消息发送;
● 可扩展性强:接入MQ组件,下游可以随意扩展事务被动方;
● 最终一致性强:写入存储的消息,最终一定可以落到MQ组件,借助MQ重试和私信队列等机制,可以保证消息最终一致;
缺点:
● 合适的锁/选举机制:监测服务需要规避单点部署,多点部署会导致存储日志多次消费写入MQ,需要引入锁/选举机制保障一次事件消息尽量只有一个单点被监测到写入MQ;
● 消息重复:事件消息保障at least once,需要事务被动方可重入;
QQ音乐当前有些业务使用该机制监测mysql和mongodb的事件,保障业务最终一致性。但需要业务关键路径所用存储支持watch机制,有所局限;同时由于监测服务多节点运行引入了数据一致性、竞争和冲突、性能和资源消耗等问题。
3、消息队列
消息队列(MQ,Message Queue)是一种分布式消息传递系统,用于在分布式系统中实现异步通信和解耦应用组件之间的依赖关系。它允许应用程序通过发送和接收消息来进行通信,而不需要直接依赖和了解彼此。与MQ事务消息机制对比,MQ事务消息需要MQ组件支持半消息和消息确认机制,MQ消息不需要。
与本地共享内存机制类似流程,由写共享内存变更为MQ组件,核心执行流程:
\1) 事务主动方收到请求后先将事件写入MQ,MQ写入成功后执行关键事务;
\2) 监测服务消费事件消息,查询关键事务存储数据,确定是否执行成功,如果失败直接放弃该事件消息;
\3) 按照预期子事务顺序执行子事务,子事务执行失败可以退避重试或入重试队列;
与本地共享内存方案对比,数据分布式存储,规避单机丢失数据风险,同时支持业务指定串并行执行子事务,可以平行扩展子事务,在最终一致场景更加松耦合和简单。
优点:
● 简单:MQ组件非常成熟,且具备比较高可用性和高性能;
● 松耦合:子事务和关键事务完全结偶,由业务定义子事务串并行关系;
● 缓冲消峰:MQ可以作为缓冲区,处理生产者和消费者之间的速率差异。它可以缓冲和平滑处理高峰期的消息流量,避免系统过载和资源浪费;
● 时序保障:MQ通常可以保证消息的顺序性。子事务可以按照消息的顺序进行处理,以确保消息的顺序性和一致性;
缺点:
● 存储压力大:所有子事务都需要查询关键事务存储状态做对账,增加关键存储压力;
● 复杂的调试和故障排除:当系统中涉及消息队列时,调试和故障排除可能变得更加复杂。追踪消息的流动、理解消息处理的状态和处理错误可能需要额外的工作和工具支持;
4、幂等可重入
幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同,做到重复操作最终也能一致,下面列举重复场景:
● 前端重复提交:用户快速重复点击多次,造成后端生成多个内容重复的订单;
● 接口超时重试:接口超时不确定被调方是否执行成功,需要重试保障投递成功;
● 消息重复消费:MQ消息中间件,消息重复生产和消费;
前置知识:一些场景可以通过状态监测,保障状态最终一致,比如评论数量和评论列表长度一致,数量可以直接重放;有些场景需要通过全局唯一id来实现幂等,比如用户购买商品和商品已售数量,商品已售数量无法简单通过用户购买商品场景获取,这个场景需要全局唯一id和幂等流程设计来达到最终一致。
下面是一些实现幂等性的常见方法:
● 更新前检查:接口先检查前置事件是否已执行成功,如果已执行成功,执行后续任务保障状态一致。比如关注场景,需要修改关注列表和粉丝列表,如果关注列表内没有执行成功,可以不执行粉丝列表更新操作;也可以检查唯一id是否已执行成功,成功就返回成功,否则执行事务;
● token机制:token机制核心解决上游重放,流程如下:
○ 上游请求token服务查询token,token服务把token存储起来并返回;
○ 上游再携带对应token请求事务服务,如果token校验不存在,则返回无效参数错误;如果token存在则删除token再执行事务;
● 锁机制:悲观锁/乐观锁/分布式锁等,将资源锁起来,获取锁成功的执行事务;获取锁失败不执行;
● 数据库去重表:引入唯一id,对于重复的id,唯一索引返回失败;
● 状态机机制:一个完整事务拆分为多个子事务,每个子事务执行完成后记录进度,对于已完成的事务不再执行,执行未完成的子事务,至到完整事务完成;
5、项目演练
抢购场景:商场线上一批数量有限的商品(每种商品数量有限)促销,每位用户限购最多2件,需要记录每宗商品售卖数量、用户购买历史。
分析:
\1.需求分解:
● 事务A:商品库存管理
● 事务B:用户购买次数管理
● 事务C:用户支付
● 事务D:商品已售数量管理
● 事务E:用户购买历史
\2.问题分析:
设计多个事务,如果用本地事务,可以一次原子操作保障一致性,但面临抢购高并发场景遇到可用性问题。按照最终一致方案来分析设计,先设计流程:
● 前端调用:引入token机制,避免用户反复操作带来的无效请求;引入全局唯一id:订单id,能够更新前检查订单状态;
● 后台流程:
如果先扣库存,遇到用户次数没有购买次数会有异常。边界场景是所有库存都被没有购买条件的用户锁住了库存,商品售卖被影响(黑产攻击);
如果先用户支付,遇到商品没有库存会影响用户体验,同时退款复杂度高(一般支付是第三方扣费);
从用户体验和安全角度,设计先扣用户购买次数,再扣商品库存,之后再用户支付;用户支付成功后再增加商品已售数量和修改用户购买历史;流程如图:
具体流程如下:
\1) 请求token管理服务获取token;
\2) 按照选择商品请求订单管理服务,获取订单id(全局唯一),订单管理服务存储订单信息,并将订单信息写入订单MQ;(通过本地事务表或MQ事务消息保障订单存储与订单MQ一致),整体依托订单id完成状态机跟进订单进度;
\3) 调用购买次数管理服务扣减用户次数,需保障可幂等重入以及回滚功能;
\4) 调用商品库存管理服务口径库存,需保障可幂等重入以及回滚功能;
\5) 调用支付管理服务支付商品;商品支付保障购买消息一定写入到购买成功MQ;
\6) 已售商品总数服务和用户购买历史服务订单id可重入(比如更新前检查,将订单id和数据一起更新,之后可以从数据按需删除已完成订单id);
\7) 订单对账服务消费订单MQ流水,追踪订单执行状态;如果库存不够,可以直接返还购买次数,也可以依托异步回滚购买次数;如果用户取消支付/超时未支持,同样需要回滚库存和用户购买次数;如果用户支付成功,则需要保障商品总数、用户购买历史和订单状态一定完成修改;
总之,分布式事务没有能一种万能钥匙的解决方案,只有依据实际业务场景选择合适的解决方案;同时有些概率非常非常小的不一致场景,需要评估完善引入的系统复杂度和投入成本,可能引入按时/按天告警引入人工解决成本更小,系统可维护性也会更高。 |