分布式事务场景
单体数据库不涉及网络交互,所以在多表之间实现事务是比较简单的,这种事务我们称之为本地事务。
但是单体数据库的性能达到瓶颈的时候,就需要分库(分物理实例),就会出现跨库(数据库实例)的事务需求;随着企业应用的规模越来越大,企业会进一步进行服务化改造,以满足业务增长的需求;当前微服务架构越来越流行,跨服务的事务场景也会越来越多。
这些都是分布式事务的需求。分布式事务是指是指事务的 参与者、数据资源服务器 以及 事务管理器 分别位于分布式系统的不同节点之上。
概括起来,分布式事务有三种场景:
- 跨数据库分布式事务
- 跨服务分布式事务
- 混合式分布式事务
分布式事务中涉及的参与者分布在异步网络中,参与者通过网络通信来达到分布式一致性,网络通信不可避免出现失败、超时的情况,因此分布式事务的实现比本地事务面临更多的困难。下面介绍几种常见的分布式事务解决方案。
分布式事理论
了解分布式事务前需要知道CAP理论和BASE理论。
CAP定理
- 一致性(C:Consistency):一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。
- 可用性(A:Availability):可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。
- 分区容错性(P:Partitiontolerance):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。
CAP定理: 一个分布式系统中,它只能同时满足CAP(一致性、可用性、分区容错性)中的两点。
BASE 理论
BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。
- 基本可用 : 是指通过支持局部故障而不是系统全局故障来实现的;
- Soft State : 表示状态可以有一段时间不同步;
- 最终一致: 最终数据是一致的就可以了,而不是实时保持强一致。
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
分布式事务模型
最早的分布式事务模型DTP
(X/Open Distributed Transaction Processing Reference Model) 是由X/Open
这个组织制定的, 同时还制定了一套分布式事务的标准 (XA协议),也就是了定义了规范和API接口,由各个厂商进行具体的实现;
主流数据库 Oracle、MySQL、SQLServer 等都支持 XA 规范,J2EE 中的 JTA 规范也是参照 XA 规范编写的,与 XA 协议兼容。
分布式事务模型DTP中定义了三个组件:
-
RM (Resource Managers)
:资源管理器,提供数据资源的操作、管理接口,保证数据的一致性和完整性。最有代表性的就是数据库管理系统,当然有的文件系统、MQ 系统也可以看作 RM。 -
TM (Transaction Managers)
:事务管理器,是一个协调者的角色,协调跨库事务关联的所有 RM 的行为。 -
AP (Application Program)
:应用程序,通过TM提供的接口 来定义事务的边界;
AP应用程序 按照业务规则调用 RM 接口来完成对业务模型数据的变更,TM 负责协调参与事务的各个 RM 一同完成一个全局事务。
XA 是在资源管理层面实现的分布式事务模型,对业务的入侵度较低。
2PC 两阶段提交
2PC (two phrase commit)是一种实现分布式事务的简单模型,这两个阶段分别是:
1)预备阶段
TM 向各个 RM事务参与者发起请求,询问是否可以执行准备操作。
RM 收到指令后,评估自己的状态,尝试执行本地事务的预备操作:预留资源,为资源加锁、执行操作等,但是并不提交事务,并等待 TM 的后续指令。
如果尝试失败则告知 TM 本阶段执行失败并且回滚自己的操作,然后不再参与本次事务(以 MySQL 为例,这个阶段会完成资源的加锁,redo log 和 undo log 的写入)。
2)提交/回滚阶段
如果协调者 收到所有参与者 准备成功的回复,则协调者向所有参与者发起事务提交操作,所有参与者收到后各自执行本地事务提交操作并向协调者发送 ACK;
如果任何一个参与者回复 准备失败 或者超时未回复,则协调者向所有参与者发起事务回滚操作,所有参与者收到后各自执行本地事务回滚操作并向协调者发送 ACK。
2pc(两阶段提交) :
2pc(两阶段提交)可以说是分布式事务的最开始的样子,像极了媒婆,就是通过消息中间件协调多个系统,在两个系统操作事务的时候都锁定资源但是不提交事务,等两者都准备好了,告诉消息中间件,然后再分别提交事务。
优点
2PC 的优势在于对业务对业务侵入很小,可以利用实现了 XA 协议的数据库 自身机制来进行事务的提交和回滚;
缺点
1)同步阻塞,性能差:
在准备阶段,要等待所有的参与者返回确认,才能进入阶段二; 在这期间,各个参与者需要将所需资源全部锁定 。
如果本地的其他请求要访问同一个资源,必须等待 参与者 收到提交/回滚命令执行完事务 释放资源后,这个请求才能得以继续执行。因为存在这种同步阻塞问题,所以影响了各个参与者的本地事务并发度;
2)单点故障:
如果 事物协调者 在某一时刻宕机,整个事务就执行不下去了,还会导致参与者长时间收不到阶段二的请求而长期持有资源的锁,影响业务的吞吐能力。
3)数据不一致问题
XA 两阶段协议可能会造成数据不一致的异常,假如 TM 在阶段二 通知 RM 提交事务时,如果指令发出后就宕机了,而只有部分 RM 收到了提交请求,然后只有那些收到命令的参与者提交事务了,此时就产生了数据不一致的问题。
小结
它是一个同步阻塞的强一致性两阶段提交协议,分别是准备阶段和提交/回滚阶段。
2PC 的优势在于对业务没有侵入,可以利用实现了 XA 协议的数据库自身机制来进行事务的提交和回滚。
它的缺点:是一个同步阻塞协议,会导致高延迟和性能的下降,并且存在协调者单点故障问题,极端情况下会有数据不一致的问题。
严格保障事务 ACID
特性是一把双刃剑。 在事务执行过程中需要将所需资源全部锁定,它更加适用于执行时间确定的短事务。 对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务系统并发性能衰退明显。 因此,在高并发的性能至上场景中,基于XA协议的分布式事务并不是最佳选择。
3PC
针对 XA 两阶段提交中的问题,有人提出了三阶段提交的改进方案,三阶段提交方案主要解决了单点故障问题,在 RM 事务参与者 侧也引入了超时机制,解决了 2PC 的同步阻塞问题。但是三阶段提交方案依然无法避免 数据不一致的异常情况出现,实际应用案例很少
TCC(Try、Confirm、Cancel)
上面提到,2PC 要求RM参与者实现了 XA 协议,通常用来解决多个数据库之间的事务问题,比较局限,例如使用实现了 XA 协议的数据库作为参与者可以完成 2PC 过程。
但在多个系统服务利用 api 接口相互调用的时候,就不遵守 XA 协议了,这时候 2PC 就不适用了。现代企业多采用分布式的微服务,因此更多的是要解决多个微服务之间的分布式事务问题。
不管是 2PC 还是 3PC 都是依赖于数据库的事务提交和回滚,而 TCC 就是一种业务层面或者是应用层的两阶段提交。TCC 分为指代 Try、Confirm、Cancel ,也就是业务层面需要写对应的三个方法,主要用于跨数据库、跨服务的业务操作的数据一致性问题。
TCC 也是一种两阶段提交协议,可以看作 2PC/XA 的一种变种,但是不会长时间持有资源锁。
TCC 模型将事务的提交划分为两个阶段:
1)阶段 1
完成业务检查(一致性)、预留业务资源(准隔离性),即 TCC 中的 try。
2)阶段 2
如果 try 阶段所有业务资源都预留成功,则执行 confirm 操作,否则执行 cancel 操作:
-
confirm:不做任何业务检查,仅仅使用预留的资源执行业务操作,如果失败会一直重试。
-
cancel:主要是在业务执行错误,需要回滚的状态下,执行业务取消操作,释放预留的资源,如果失败会一直重试。
TCC (Try、Commit、Cancel) 是一种补偿型事务,该模型要求应用的每个服务提供 try、confirm、cancel 三个接口,它的核心思想是通过资源的预留(提供中间态),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源。
缺点:
TCC 事务模型对业务方侵入较大,需要业务方把功能的实现上由一个接口拆分为三个,开发成本较高。
注意点
同时 TCC 事务为了解决异步网络中的通信失败或超时带来的异常情况,要求业务方在设计实现上要遵循三个策略:
-
允许空回滚:如果在阶段 1 时发生网络异常,部分参与方没有收到 try 请求而超时了,从而触发整个事务的 cancel 操作,try 失败或者没有执行 try 操作的参与方收到 cancel 请求时,要保证空回滚操作能正常进行。
-
保持幂等性:在阶段 2 时,比如出现网络超时或执行失败的情况,会重复调用参与方的 confirm/cancel 方法,因此这两个方法实现上需要保证幂等性,避免重复执行产生错误。
-
防止资源悬挂:出现网络异常导致两个阶段无法保证严格的顺序执行时,如果出现 try 请求比 cancel 请求更晚到达的情况, 参与者按照 Cancel 指令 执行空回滚确保事务的正确性,对于事务管理器来说这时候事务已经是结束了的,此时再执行Try 操作 ,相关预留资源就被“悬挂”了;所以空回滚之后还得记录一下,防止 Try 操作的再调用。
举例
描述 TCC 方案使用的电商微服务模型如下图所示,在这个模型中,shopping-service 是事务协调者,repo-service 和 order-service 是事务参与者。
我们以一个简单的电商系统(最简单的购物业务流程,后续的积分赠送、物流等业务不在考虑范围内)为例,小明在淘宝上花 100 元买了一件商品,业务流程上有如下几个操作:
-
订单系统创建商品订单
-
支付系统接受小明的支付
-
库存系统扣减产品库存
这几个动作需要作为一个事务执行,要同时成功或者同时撤销。
如果采用 TCC 事务模式,那么各个系统需要改造为如下状态:
1)订单系统
- try:创建一个订单,状态显示为“待支付”
- confirm:更新订单的状态为“已完成”
- cancel:更新订单的状态为“已取消”
2)支付系统
- try:假设小明账户中有 1000 元,冻结小明账户中的 100 元,此时小明看到的账户余额依然是 1000 元,但是可用余额变成了900元 。
- confirm:将账户余额变为 900 元,并清除冻结记录。
- concel:清除冻结记录,释放冻结金额。
3)库存系统
- try:假设库存中还剩 10 本书,冻结其中的一本书,此时可售库存为9本。
- confirm:将剩余库存更新为 9 本书,并清除冻结记录。
- cancel:清除冻结记录,释放冻结库存数量。
基于消息的分布式事务(最终一致性)
无论是 2PC & 3PC 还是 TCC,基本都遵守 XA 协议的思想。即这些方案本质上都是事务协调者 协调 各个事务参与者的本地事务的进度,使所有本地事务共同提交或回滚,最终达成一种全局的 ACID 特性。在协调的过程中,协调者需要收集各个本地事务的当前状态,并根据这些状态发出下一阶段的操作指令。
但是这些全局事务方案由于操作繁琐、时间跨度大,或者在全局事务期间会排他地锁住相关资源,使得整个分布式系统的全局事务的并发度不会太高。这很难满足电商等高并发场景对事务吞吐量的要求,因此互联网服务提供商探索出了很多与 XA 协议背道而驰的分布式事务解决方案。其中利用消息中间件实现的最终一致性全局事务就是一个经典方案。
基于消息的分布式事务模式核心思想是 通过 消息系统 来通知 其他事务参与方 自己事务的执行状态,通过消息传递的方式保证事务的最终一致性。
消息系统的引入更有效的将事务参与方解耦,各个参与方可以异步执行。
该种模式的难点在于解决本地事务执行和消息发送的一致性:两者要同时执行成功或者同时取消执行。
实现上主要有两种方式:
-
基于事务消息的方案
-
基于本地消息的方案
基于事务消息的方案
普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
解决这个问题,需要引入事务消息,事务消息和普通消息的区别在于事务消息发送成功后,处于 prepared 状态,不能被订阅者消费,等到事务消息的状态更改为可消费状态后,下游订阅者才可以监听到次消息。
本地事务和事务消息的发送的处理流程如下:
- 事务发起者预先发送一个事务消息。
- MQ 系统收到事务消息后,将消息持久化,消息的状态是“待发送”,并给发送者一个 ACK 消息。
- 事务发起者如果没有收到 ACK 消息,则取消本地事务的执行;如果收到了 ACK 消息,则执行本地事务,并给 MQ 系统再发送一个消息,通知本地事务的执行情况。
- MQ 系统收到消息通知后,根据本地事务的执行情况更改事务消息的状态,如果成功执行,则将消息更改为“可消费”并择机下发给订阅者;如果事务执行失败,则删除该事务消息。
- 如果事务消息的状态是“可发送”,则 MQ 系统向下游参与者推送消息,推送失败会不停重试。
- 下游参与者收到消息后,执行本地事务,本地事务如果执行成功,则给 MQ 系统发送 ACK 消息;如果执行失败,则不发送 ACK 消息,MQ 系统会持续推送给消息。
- 本地事务执行完毕后,发给 MQ 的通知消息有可能丢失了。
所以支持事务消息的 MQ 系统有一个定时扫描逻辑,扫描出状态仍然是“待发送”状态的消息,并向消息的发送方 询问这条事务消息的最终状态如何,并根据结果更新事务消息的状态。因此事务的发起方需要给 MQ 系统提供一个事务消息状态查询接口。
整个流程中,我们能保证:
- 事务发起方本地事务提交失败,下游事务参与者不会收到消息的投递。
- 只要事务发起方本地事务执行成功,那么消息服务一定会投递消息给下游的事务参与者,并最终保证下游的事务参与者一定能成功消费该消息(消费成功或失败,即最终一定会有一个最终态)。
基于本地消息的方案
基于事务消息的模式对 MQ 要求较高,并不是所有 MQ 系统都支持事务消息的,如果所依赖的 MQ 系统不支持事务消息,那么可以采用本地消息的分布式模式。
该种模式的核心思想是事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。
- 业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。
- 系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到 MQ 系统中,如果发送失败或者超时,则一直发送,直到发送成功后,从本地消息表中删除该记录。
后续的消费订阅流程则与基于事务消息的模式雷同。
小结:
基于消息中间件的最终一致性全局事务方案是互联网公司在高并发场景中探索出的一种创新型应用模式,利用 MQ 实现微服务之间的异步调用、各个事务参与方之间的调用不再是同步调用,可以有效的解耦和流量削峰,支持全局事务的高并发,并保证分布式数据记录的最终一致性。
对 MQ 系统的要求较高,对业务实现也有一定的侵入性,要么提供事务消息状态查询接口,要么需要维护本地消息表。并且原则上只接受下游分支事务的成功,不接受事务的回滚,如果失败就要一直重试,适用于对最终一致性敏感度较低的业务场景。
参考:
https://mp.weixin.qq.com/s/2AL3uJ5BG2X3Y2Vxg0XqnQ
https://mp.weixin.qq.com/s/ujRRtdLOeKEHsHrtDRNXGA
https://mp.weixin.qq.com/s/OKon95MRUqDc9IwtEqPSjQ