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