分布式事务基础
事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务有四个特性,习惯上被称为 ACID 特性:
- Atomicity(原子性)
- Consistency(一致性)
- Isolation(隔离性)
- Durability(持久性)
本地事物
在系统发展初期,单体应用对应一个数据库,整个服务操作只涉及一个数据库资源,通过数据库自带的事务很容易实现 ACID,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
分布式事务
随着互联网的发展,微服务架构大规模的普及,软件系统由原来的单体应用转变为分布式应用。分布式系统一般由多个独立的子系统组成,多个子系统通过网络通信互相协作配合完成各个功能。
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。比如在一个电商系统中,一条订单的生成涉及库存、订单、支付等不同的服务,不同的服务之间要么全成功、要么全失败,保证事务的 ACID 特性。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
在分布式系统中数据一致性又可以划分出多个一致性模型
强一致性:任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。
弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
最终一致性:不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
在解决分布式事物的数据一致性问题上,产生了多个相关的理论。
CAP 理论
CAP 定理又被称作布鲁尔定理,是加州大学的计算机科学家布鲁尔在 2000 年提出的一个猜想。2002 年,麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。
- C : Consistency 一致性 , 所有实例节点同一时间看到是相同的数据
- A : Availability 可用性 , 不管是否成功,确保每一个请求都能接收到响应
- P : Partition tolerance 分区容错性 , 系统任意分区后,在网络故障时,仍能操作
CAP 理论是指在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性、可用性、分区容错性者中的两个,另外一个必须被牺牲。
在真实的分布式环境下,如果我们选择了 CA(一致性 + 可用性) 而放弃了 P(分区容错性),那么当发生分区现象时,为了保证 C(一致性),系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A(可用性) 冲突了,因为 A(可用性)要求返回 no error 和 no timeout。因此虽然在 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来,必须选择 P(分区容错)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。
也就说在真是环境下我们只能选择 CP(一致性 + 分区容错性) 或者 AP (可用性 + 分区容错性)架构,在一致性和可用性做折中选择。
虽然 CAP 理论告诉我们分布式系统只能选择 AP 或者 CP,但实际上并不是说整个系统只能选择 AP 或者 CP,在 CAP 理论落地实践时,我们需要将系统内的数据按照不同的应用场景和要求进行分类,每类数据选择不同的策略(CP 还是 AP),而不是直接限定整个系统所有数据都是同一策略。
BASE 理论–CAP 理论的延伸
由于在分布式系统中 C、A、P 三者都无法抛弃,但 CAP 定理限制三者无法同时满足,这种情况,我们会选择尽量靠近 CAP 定理,即尽量让 C、A、P 都满足,在此所趋下,出现了 BASE 定理。
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。
- BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。 - S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。 - E: Eventual Consistency 最终一致性, 系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
BASE和ACID的区别与联系
- ACID是传统数据库常用的设计理念, 追求强一致性模型。
- BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性
ACID和BASE代表了两种截然相反的设计哲学。
总的来说,BASE 理论面向大型高可用可扩展的分布式系统,与ACID这种强一致性模型不同,常常是牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。虽然两者处于(一致性-可用性)分布图的两级,但两者并不是孤立的,对于分布式系统来说,往往依据业务的不同和使用的系统组件不同,而需要灵活的调整一致性要求,也因此,常常会组合使用ACID和BASE。
柔性事务
不同于 ACID 的刚性事务,在分布式场景下基于 BASE 理论,就出现了柔性事务的概念。柔性事务下,在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐。
XA –强一致性
由 Tuxedo 提出的 XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。XA 协议可以分为两部分,即事务管理器和本地资源管理器。
- 事务管理器作为
协调者
,负责各个本地资源的提交和回滚。 - 资源管理器就是分布式事务的
参与者
.其中资源管理通常是数据库。
基于 XA 协议的,发展出了二阶段提交协议(The two-phase commit protocol,2PC)和三阶段提交协议(Three-phase commit protocol,3PC)。
2PC
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。
0x1 准备阶段
- 协调者向所有参与者发送 CanCommit 操作请求,并等待参与者的响应。
- 参与者接收到请求后,会执行请求中的事务操作,将 undo 和 redo 信息记入事务日志中,但是这时并不提交事务。
若不成功,则发送“No”消息,表示终止操作。当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了提交阶段。
0x2 提交阶段
协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令
若协调者收到的都是“Yes”消息,则向参与者发送“DoCommit”消息,参与者会提交事务并释放资源,然后向协调者返回“Ack”消息。
如果协调者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息,此时发送“Yes”的参与者会根据之前执行操作时的回滚日志对操作进行回滚,然后所有参与者会向协调者发送“Ack”消息;
- 协调者接收到所有参与者的“Ack”消息,就意味着整个事务结束了。
2PC 实现起来比较简单,但是实际项目中使用比较少,主要因为以下问题:
性能问题:所有参与节点都是事务阻塞型的,占用系统资源,容易导致性能瓶颈。
可靠性问题:如果协调者出现故障,参与者将一直处于锁定状态。
数据一致性问题:在提交阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3PC
基于 2PC 基础上,3PC 对 2PC 进行了改进,引入了超时机制。同时将准备阶段拆分为 2 个阶段,多了一个 PreCommit 阶段。
3PC 可以划分为 CanCommit 阶段、PreCommit 阶段、DoCommit 阶段。
0x1 CanCommit 阶段
- 协调者向所有参与者发送 “CanCommit” 请求,询问是否可以提交事务,并等待所有参与者答复。
- 参与者收到 “CanCommit” 请求之后,回复 “Yes”,表示可以顺利执行事务;否则回复 “No”。
0x2 PreCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作或中断事务。
如果参与者返回的回复情况全部是 Yes
- 协调者向所有参与者发送 “PreCommit” 请求,参与者进入到预提交阶段。
- 参与者收到 “PreCommit” 请求后,执行事务操作,并将 undo 和 redo 信息记入事务日志中,但这时并不提交事务。
- 参与者向协调者反馈执行成功 “Yes” 或失败响应 “No”。
如果参与者返回的回复情况中包含 No,说明有一个事务执行失败。
- 协调者向所有参与者发送 “Abort”请求
- 参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。
0x3 DoCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 DoCommit 操作 或 中断事务。
如果参与者返回的回复情况全部是 YES
- 协调者向所有参与者发送 “DoCommit” 消息。
- 参与者接收到 “DoCommit” 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源。
- 参与者提交完事务之后,向协调者发送 “Ack” 响应
- 协调者接收到所有参与者的 “Ack” 响应之后,完成事务。
如果参与者返回的回复情况中包含 No,说明有一个事务执行失败。
- 协调者向所有参与者发送 “Abort” 请求。
- 参与者接收到 “Abort” 消息之后,利用其在 “PreCommit” 阶段记录的 undo 信息执行事务的回滚操作,并释放所有锁住的资源。
- 参与者完成事务回滚之后,向协调者发送 “Ack” 消息。
- 协调者接收到参与者反馈的 “Ack” 消息之后,执行事务的中断,并结束事务。
相比二阶段提交,三阶段降低了阻塞范围,在等待超时后协调者或参与者会中断事务,避免了协调者单点问题。DoCommit 阶段中协调者出现问题时,参与者会继续提交事务。
但是数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 DoCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC –最终一致性
TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC 是服务化的二阶段编程模型, 针对每个操作,都要实现对应的确认和补偿操作,也就是业务逻辑的每个服务都需要实现 Try、Confirm、Cancel 三个操作,第一阶段由业务代码编排来调用 Try 接口进行资源预留,当所有参与者的 Try 接口都成功了,事务协调者提交事务,并调用参与者的 Confirm 接口真正提交业务操作,否则调用每个参与者的 Cancel 接口回滚事务,并且由于 Confirm 或者 Cancel 有可能会重试,因此对应的部分需要支持幂等。
- Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
- Confirm 阶段: 确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性
- Cancel 阶段: 取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:
- 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
- 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
- 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
但是TCC中Try、Confirm、Cancel 的操作需要业务来实现,耦合度过高。
本地消息表 –最终一致性
本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。核心思路是将分布式事务拆分成本地事务进行处理。 本地事物表方案可以将事务分为事务主动方和事物被动方。
- 事务主动方: 分布式事务最先开始处理的事务方
- 事务被动方: 在事务主动方之后处理的业务内的其他事务
事务的主动方需要额外新建事务消息表
,用于记录分布式事务的消息的发生、处理状态。整个业务流程:
- 事务主动方在本地事务中处理业务更新操作和写消息表操作。
- 事务主动方通过消息中间件,通知事务被动方处理事务。
- 事务被动方通过消息中间件,通知事务主动方事务已处理的消息
本地消息表实现的条件:
- 消费者与生成者的接口都要支持幂等
- 生产者需要额外的创建消息表
- 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作
容错机制:
- 步骤 1 失败时,事务直接回滚
- 步骤 2、3 写 mq 与消费 mq 失败会进行重试
- 步骤 3 业务失败事务被动方向事务主动方发起事务回滚操作
MQ 事务 –最终一致性
有些 MQ 的实现支持事务,比如 RocketMQ ,基于 MQ 的分布式事务方案其实是对本地消息表的封装。以 RocketMQ 为例介绍 MQ 的分布式事务方案。
- 发送方向 MQ 服务端(MQ Server)发送 half 消息。这个 half 消息与普通消息的区别在于,在事物提交之前,这个消息对订阅方来说是不可见的,订阅方不会消费这个消息。
- MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
- 发送方开始执行本地事务逻辑。
- 如果事务提交成功,将会发送确认消息(commit 或是 rollback)至 MQ Server。
- MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除half消息,订阅方将不会接受该消息。
异常情况 1:如果发送方发送 commit 或 rollback 消息失败,未到达消息集群
- MQ Server 会发起消息回查
- 发送方收到回查消息后,会检查本地事务的执行结果
- 根据本地事务的执行结果重新发送 commit 或 rollback 消息
- MQ Server 根据接收到的消息(commit 或 rollback)判断消息是否可消费或直接删除
异常情况 2:接收方消费失败或消费超时
- 一直重试消费,直到事务订阅方消费消息成功,整个过程可能会导致重复消费问题,所以业务逻辑需要保证幂等性
异常情况 3:消息已消费,但接收方业务处理失败
- 通过 MQ Server 通知发送方进行补偿或事务回滚
Saga 事务 –最终一致性
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文,Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 事务基本协议如下:
每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
Saga 的执行顺序有两种:
- T1, T2, T3, …, Tn
- T1, T2, …, Tj, Cj,…, C2, C1,其中 0 < j < n
Saga 定义了两种恢复策略:
- 向前恢复(forward recovery)
适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中 j 是发生错误的子事务(sub-transaction)。该情况下不需要 Ci。
- 向后恢复(backward recovery)
如果任一子事务失败,补偿所有已完成的事务。即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种做法的效果是撤销掉之前所有成功的 sub-transation,使得整个 Saga 的执行结果撤销。
Saga 事务常见的有两种不同的实现方式:
- 命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序
- 事件编排 (Event Choreography):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。