在当前的技术发展阶段,不同的业务场景对一致性、可靠性、易用性、性能等要求不同,应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。行业中把分布式事务解决方案分为刚性事务方案和柔性事务方案这两大类。
就刚性事务这个范畴,The Open Group 的组织提出的 X/Open Distributed Transaction Processing (DTP) Model ,已经成为事实上的事务模型组件的行为标准,解决了不少分布式事务的问题。但随着技术以及业务场景的演进发展,其缺点和局限性也越来越明显,伴随着 BASE 理论的提出,诸多柔性事务方案(如可靠消息、最大努力通知、AT、TCC、Saga)诞生,满足了各类业务场景的差异化需求。
一、XA(2PC) 规范和 DTP 模型介绍
1.1、 前奏- 2 阶段提交协议(2PC)
在开始介绍 DTP 模型之前,需要先了解 两阶段提交协议。2 阶段提交协议(The two-phase commit protocol)简称 2PC,2PC 是为了要保证分布式事务的原子性,此协议中有【协调者】和【参与者】两个角色,整个处理过程包含两个阶段:投票阶段 和 提交阶段:
- 投票阶段
- 协调者发送事务预备(Prepare)请求到所有的参与节点,并询问它们是否可以提交。
- 提交阶段
- 依据第 1 阶段返回的结果,决定事务最终是提交(Commit)还是放弃(Rollback)。
- 如果所有的参与节点都回复 Yes,那么协调者在第 2 阶段发出提交(Commit)请求。
- 如果任一参与节点回复“No”,那么协调者在第 2 阶段发出回滚(Rollback)请求。
整个过程要么成功,要么失败回滚,对外部来说,该事务的状态变化是原子的。要达到这个效果,对 2 个阶段的处理是有约束的:
- 在第 1 阶段,当事务的参与者回复“Yes”的时候,对于当前事务,这个参与者一定是能够安全提交的
- 在第 2 阶段,当协调者基于参与者的投票,做出提交或者回滚的决定后,这个决定是不可以撤销的
1.2、 DTP 模型介绍
X/Open 组织定义的分布式事务的处理(DTP)模型由以下几个元素组成:
AP(Application 应用程序)
- 用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作
TM(Transaction Manager 事务管理器)
- 负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等
RM(Resource Manager 资源管理器)
- 如数据库、文件系统等,并提供访问资源的方式
CRM(Communication Resource Manager 通信资源管理器)
- 控制一个 TM 域(TM domain)内或者跨 TM 域的分布式应用之间的通信
CP(Communication Protocol 通信协议)
- 提供 CRM 提供的分布式应用节点之间的底层通信服务
在 DTP 中,基本组成需要涵盖 AP、TM、RMs(不需要 CRM、CP 也是可以的),如下图所示
1.3 XA 规范介绍
XA 规范是 X/Open 组织提出的分布式事务处理 (DTP)的接口规范(所以也常看到被叫做 XA 接口 ),描述了 RM 要支持事务性访问所必需做的事情,XA 接口在 TM 与 RM 之间提供双向通信。它是 TM、RM 这两个 DTP 软件组件之间的系统级别接口,而不是应用程序开发者对其进行编码的普通应用程序接口。
重要提示:为什么有的资料把 XA 叫做 2PC ,把 XA 和 2PC 描述为同一个事物。DTP、XA、2PC 之间是什么关系?可以这么理解,DTP 模型定义 TM 和 RM 之间通讯的接口规范叫 XA,XA 规范使用 2PC 来保证事务中的所有资源全部提交或全部回滚。
二、MySQL 对 XA 规范的支持
DTP 模型定义了 TM 和 RM 之间通讯的 XA 规范,而 DB 在 DTP 模型中担任 RM 的角色。为了让上层应用可以便捷的使用分布式事务,许多关系型 DB 都实现了 X/Open 的 XA 规范。MySQl 从 5.x 开始支持 XA 规范,但仅 innodb 存储引擎支持 。
2.1 MySQL XA 命令介绍
这里列举出 MySQL XA 的命令集合,先有个初步认识;看完后边状态流转和示例后可加深印象。
命令 | 解释 |
---|---|
XA START xid | 开启一个事务,并将事务置于 ACTIVE 状态,此后执行的 SQL 语句都将置于该事务中 |
XA END xid | 将事务置于 IDLE 状态,表示事务内的 SQL 操作完成 |
XA PREPARE xid | 实现事务提交的准备工作,事务状态置于 PREPARED 状态。事务如果无法完成提交前的准备操作,该语句会执行失败 |
XA COMMIT xid | 事务最终提交,完成持久化 |
XA ROLLBACK xid | 事务回滚终止 |
XA RECOVER | 查看 MySQL 中存在的 PREPARED 状态的 xa 事务 |
2.2 事务状态转换
XA 事务可以一步提交(非本篇关注点),本篇重点是两阶段提交。对于两阶段提交来说,要在 prepare 之后等其他 RM 反馈结果。状态转换流程如下图所示:
- XA 事务开启时多了一个全局事务号,结束时多了一个 end 动作 和 prepare 动作
- XA START, 开启一个分布式事务,需要指定分布式事务号
- XA END,在内部仅是一个状态变化,声明当前 XA 事务结束,不允许追加新的 sql 语句,无其它作用,业界有人提出 XA 事务框架去掉这一步,减少一次网络交互,提高性能
- XA PREPARE,MySQL 5.7 版本此指令才写 binlog 和 redo log,预提交事务,并将分布式事务信息保存到全局内存结构,让其它连接可以查询、回滚、提交
- XA COMMIT,真正提交事务,修改事务状态,释放锁资源。如果实例上 XA PREPARE 已经成功,那么它的 XA COMMIT 一定能成功
2.3、一个简单的示例:
// 第一阶段
// 1.1 通过 XA START 和 XA END 来包裹 用户业务SQL
mysql> XA START 'transfer_money';
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET money = money -100 where id = 1 ;
Query OK, 1 row affected (0.00 sec)
mysql> XA END 'transfer_money';
Query OK, 0 rows affected (0.00 sec)
// 1.2 通过 XA PREPARE 通知 RM 一阶段就绪
mysql> XA PREPARE 'transfer_money';
Query OK, 0 rows affected (0.00 sec)
// 第二阶段,通过 XA COMMIT 完成二阶段的提交
mysql>
mysql> XA COMMIT 'transfer_money';
Query OK, 0 rows affected (0.00 sec)
2.4、MySQL XA 事务异常处理
XA 事务 prepare 成功后,若不提交就相当于是长事务了,要尽快提交,否则会带来很多问题。但 DB 或应用出错导致 XA 事务未能全部提交的情况也无法避免,这些残存 XA 事务可人工处理,执行 xa recover
查看未提交 XA 事务,选择对应的进行 rollback 或 commit。
2.5、java 如何使用 MySQL XA 方案
java 项目中若使用 MySQL XA 方案,有 JTA(Atomikos 框架提供支持) 和 Seata 的 XA 模式,这两种实现方式。
JTA(Atomikos)
- JTA(Java Transaction API)是 java 根据 XA 规范提供的事务处理标准,目的是统一一套 API 简化学习,JTA 和它的同胞 JTS(Java Transaction Service),为 J2EE 平台提供了分布式事务服务
- JTA 的目标是屏蔽底层事务资源,使应用可以透明的方式参入到事务处理中(Seata 的也类似)
* 非常有名的分布式事务开源框架 atomikos 提供了开源免费的 JTA/XA 实现(也有 TCC 机制的商业付费版实现)。接入简单,只需引入对应 jar 包,无需额外的服务。但 JTA 方案仅适用于单体架构多数据源时实现分布式事务
Seata XA 模式
- Seata XA 模式可应对多个微服务间的分布式事务,使用时除了需要引入依赖 Jar,还需要启动 Seata Server 这个额外的分布式事务协调者服务
三、XA(2PC) 的优缺点
2PC 是 XA 规范用于在全局事务中协调多个资源的机制。2PC 遵循 OSI(Open System Interconnection,开放系统互联)/DTP 标准,但实际它比标准本身还要早若干年出现。
网络中关于 2PC 的描述有的是仅指 2PC 算法本身,而有的则用 2PC 来代指 XA;依笔者目前有限的认知来看,网络资料中大多关于 2PC 缺点的描述,是特指 XA(即强一致方案下的 2PC)。因为目前其他主流的如 TCC、AT 等模式也属于是 2PC 的变种,但其出现是为了解决了 XA 已知的诸多问题,在阅读资料时需根据上下文来辨识。
3.1 XA(2PC) 的优点
- 业务无侵入:XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担
- 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用
3.2 XA(2PC) 的缺点
数据锁定
- 使用 XA 事务时,数据在整个事务的二阶段处理结束前都被锁定,数据的读写都遵守 DB 隔离级别的定义
协议阻塞
- XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。但协议阻塞等待的背后问题实际是资源的锁等待, 如果一个 RM 收不到分支事务二阶段的指令,那么它锁定的数据,将一直被锁定,其他事务访问此资源时就不得不也阻塞等待,可能出现死锁。这是 XA 的核心痛点,这个痛点中协调者的可用性是个关键,Seata 主要是解决了失联问题,并通过增加自解锁机制来解决这个问题(后续文章补充)
性能差
- 因为以上两点,事务 2 阶段的处理过程,增加单个事务的 RT;并发事务数据的锁冲突,降低吞吐
四、3 阶段提交(3PC)
3 阶段提交在维基百科的描述是这样:
三阶段提交(英语:Three-phase commit),也叫三阶段提交协议(英语:Three-phase commit protocol),是在电脑网络及数据库的范畴下,令一个分布式系统内的所有节点能够执行事务的提交的一种分布式算法。三阶段提交是为了解决两阶段提交协议的缺点而设计的。
与两阶段提交不同的是,三阶段提交是一种“非阻塞”的协议。三阶段提交在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,令原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题[1]得以解决。
简单来说就是把 2PC 中的第 1 个阶段拆分成先询问所有参与者是否可以执行事务-得到一致的同意回复后才执行预提交并锁资源,最后真正提交。
4.1、阶段 1:CanCommit
- 协调者向各参与者发送 CanCommit 的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应
- 参与者收到 CanCommit 请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈 Yes 响应,并进入预备状态,否则反馈 No
4.2、阶段 2:PreCommit
这个环节根据阶段 1 的参与者的反馈不同,而执行不同的逻辑:
1)如果任意一个参与者在阶段 1 向协调者反馈了 No 响应,或者协调者等待参与者反馈超时,那么就会中断事务,中断执行逻辑如下:
- 协调者向所有参与者发送 abort 请求
- 参与者无论是收到来自协调者的 abort 请求,还是等待超时,都执行事务中断
2)另一种情况,如果协调者接收到各参与者反馈都是 Yes,那么才执行事务预提交,执行逻辑如下:
- 协调者向各参与者发送 preCommit 请求,并进入 prepared 阶段
- 各参与者接收到 preCommit 请求后执行事务操作,并将 Undo 和 Redo 信息记录到事务日记中,但事务不提交
- 如果各参与者都成功执行了事务操作,那么反馈给协调者 Ack 响应,同时等待最终指令,提交 commit 或者终止 abort
4.3、阶段 3:doCommit
这个环节根据阶段 2 参与者不同反馈,而执行不同的逻辑:
1)假设协调者正常工作,并且有任意一个参与者在阶段 2 反馈 No,或者在等待参与者的反馈超时后,都会主动中断事务
- 协调者向所有参与者节点发送 abort 请求
- 参与者接收到 abort 请求后,利用 undo 日志执行事务回滚,并在完成事务回滚后释放占用的资源后,向协调者发送 ack 信息,反馈事务回滚结果
- 协调者接收到所有参与者反馈的 ACK 消息之后,完成事务的中断
2)假设协调者正常工作,接收到了所有参与者的 ack 响应,那么它将从预提交阶段进入提交状态
- 协调者向所有参与者发送 doCommit 请求
- 参与者收到 doCommit 请求后,正式提交事务,并在完成事务提交后释放占用的资源,向协调者发送 ACK 信息,反馈事务提交结果
- 协调者接收到所有参与者 ack 信息,整个事务完成
4.4、3PC 的核心理念
3PC 的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。怎么展开来理解呢?
- 在锁定资源之前通过询问的方式执行一次资源锁定前的预检,可以减少非必要资源锁定
- 通过两次确认来保证在进入最后提交阶段时各参与节点的状态是一致的
- 避免无限等待,同时在协调者和参与者中都引入超时机制
- 在参与者完成 PreCommit 后,即使遇到与协调者交互超时的问题,仍可自主的继续 Commit。因为完成 PreCommit 后也意味着大家其实都在 CanCommit 阶段同意修改了,此时采取 Commit 极大的概率不会错的。而 2 阶段提交中参与者在 1 阶段后是不知所措的(不知道其他参与者怎么反馈,不知道协调者的决议是什么),而且不知所错的状态可能持续相当长的时间。
4.5、探讨 3PC 中的异常处理
在协调者和参与者中都引入超时机制,超时后自醒,主动执行自认为合理的下一步逻辑(回滚或者提交),避免无上限的挂起:
- 协调者,遇到超时就给参与者发 abort 指令,中断整个事务
- 参与者,在收到 can commit 后,执行 pre commit 之前,超时自醒后直接中断 RM 的分支事务
- 参与者,在收到 pre commit 指令后,等待下一步的 do commit 指令超时,直接提交事务(大概率是正确的选择,但如果是错误的选择就导致了数据的不一致)
1)canCommit 阶段
- 部分参与者收到 canCommit 指令后,直接反馈不能开始事务,协调者向所有参与者发送 abort 请求。
- 协调者等待反馈超时,向所有参与者发送 abort 请求。
- 参与者预检没问题,但一直无法接收到下一步的的指令(反馈 can commit 超时 或接收协调者的下一步指令超时),自省中断事务的中断
- 参与者收到来自协调者的 abort 请求之后执行事务中断
2)pre commit 阶段
- 协调者收到参与者反馈 PreCommit 异常,向所有参与者发送 abort 请求,执行回滚
- 协调者若等待参与者的 PreCommit 反馈超时,向所有参与者发送 abort 请求,执行回滚
3)do commit 阶段
- 参与者收到 PreCommit 指令并正常执行事务,给协调者反馈 preCommit 完成后,理论上是要等待下一步的 doCommit 指令后才提交事务;但是如果未能等到下一步的 doCommit 指令超时了,会自主提交事务。因为询问阶段是一致通过的,执行到这个阶段整个事务成功的概率已经很高了
- 部分参与者执行 preCommit 异常,部分参与者执行 preCommit 正常,但此时协调者挂了,那么通过参与者的超时自醒机制,就出现部分参与者提交,部分参与者回滚,出现数据不一致。
- 另外一种情况,协调者发送了 abort 指令,参与者超时未收到指令就提交了事务,其他参与者收到了协调者发送的 abort 指令后执行了回滚,也会出现数据不一致。
4.6、3PC 的优缺点
2PC 和 3PC ,它们是在分布式数据库管理系统 (Distributed Database Management System,DDBMS) 中管理如何提交或中止数据库事务的最流行算法。2PC 协议是一个阻塞的两阶段提交协议,3PC 协议是一种非阻塞的三阶段提交协议。通过询问避免了资源的无效占用;协调者和参与者各自有超时自醒处理,避免长时间资源占用不释放,提高资源利用率。消除了 2PC 协议的阻塞问题,但是,3PC 协议在网络出现分段的情况下不会恢复。因此,Keidar 和 Dolev (1998) 建议使用增强型三阶段提交 (E3PC) 协议来消除此问题。E3PC 协议需要至少三个往返才能完成,这将有很长的延迟才能完成每笔交易。
所以,无论是 3PC 还是 E3PC,虽然解决了一些问题,但遗留问题以及复杂度的提升也导致性价比不高,估计在实际应用中的效果并不好,所以目前普遍使用的依然是 XA(2PC) ;对 3PC 的学习都是理论(缺产品),通过对 3PC 理论学习探讨,来理解经典 XA(2PC) 的优缺点,并作为后续学习柔性事务方案的理论借鉴。
补充:有些网络资料描述 XA 包括 2PC 和 3PC,但从维基百科的信息来看,对 XA 的描述中并未提及 3PC,且 3PC 的描述中也未看到 XA。而且 3PC 没有让人熟悉的产品实现,后续内容就不再与其他可实施落地的方案一起讨论了。
五、柔性事务介绍
XA(2PC)由数据库做为参与者管理事务资源,提供分布式事务的处理支持,所以可以保障从任意视角对数据的访问都能有效隔离,满足全局数据一致性,被称作刚性事务。
刚性事务属于 CAP 理论中的 CP 组合,会有性能上限,无法满足高并发场景的需求。基于 BASE 理论,柔性事务方案被提出用于保证事务数据的最终一致性。柔性事务本质是对 XA 协议的妥协,它通过降低强一致性要求,从而降低数据库资源锁定时间,提升可用性,允许有中间状态,要求最终一致性,也就是 AP 组合。柔性事务分为通知型和补偿型:
- 通知型事务都是异步的,包含有:可靠消息、最大努力通知两种
- 补偿型事务都是同步的,包含有:AT、TCC、Saga
六、通知型事务
6.1、可靠消息
可靠消息方案是指当事务发起方(消息发送者)执行完成本地事务后并发出一条消息,事务参与方(消息接收者)一定能够接收消息并处理事务成功。
此方案强调的是一旦消息发给事务参与方,则最终事务要达到一致。这其中有两个关键问题:
- 本地事务与消息发送的原子性问题
- 若事务发起方在本地事务执行成功,则消息必须发出(可以是立即发送,也可以是异步发送),否则就无消息
- 事务参与方接收消息的可靠性问题
- 参与方必须能够从 MQ 接收到消息,如果接收消息失败可以重复接收消息。
- 参与方要解决消息重复消费的问题(消费处理的幂等性)。
可靠消息方案适合执行周期长且实时性要求不高的场景。引入消息机制后,原先同步的事务操作变为基于消息写/读的异步操作,避免了同步阻塞,也实现了服务间的解耦。一般有基于本地消息表和基于消息中间件这两种实现式。
1)基于本地消息表
本地消息表核心思路是:将分布式事务拆分成本地事务进行处理
- 在消息发送者一端 额外新建事务消息表
- 消息发送者一端在同一个本地事务中处理业务和写消息表操作
- 定时任务轮询事务消息表的待发送的数据,确保发送给 MQ
- 在消息消费者一端消费 MQ 消息,若消费失败则重试消费,最终达到发送者和接收者两端数据的最终一致性
优点:从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖,方案轻量,容易实现。
缺点:需设计 DB 消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。
2)基于 MQ 事务消息
为了能解决本地消息表方案的这个缺点,同时又不和业务耦合,RocketMQ 提供了“事务消息”的能力,可以理解为将本地消息表移动到了 MQ 内部。
事务消息发送步骤如下:
- 发送方将半事务消息发送至 RocketMQ 服务端
- 服务端将消息持久化成功之后,向发送方返回 ACK 确认消息已经发送成功,此时消息为半事务消息
- 发送方开始执行本地事务逻辑
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述发送步骤的步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照发送步骤的步骤 4 对半事务消息进行操作
6.2、最大努力通知
最大努力通知型的目标是 事务发起方尽量将业务处理结果通知到参与方。适用于一些最终一致性时间敏感度低,且参与方的处理结果不影响发起方的处理结果的这类通知类的业务场景。如短信供应商的回执通知:
最大努力通知型的实现方案,一般符合以下两个特点:
- 消息重复通知
- 在业务活动发起方完成业务处理之后,向参与方发送消息,参与方可能没有接收到通知,此时要发起方有一定的机制对消息重复通知(通常是发起方调用参与方的 http 接口,而且会协商一个 N 次 通知的上限)
- 定期校对
- 事务发起方提供消息校对的接口,如果事务参与方没有接收到发起方发送的消息,可以调用事务发起方提供的接口主动获取消息
6.3 最大努力通知 VS 可靠消息
1)可靠性的保障方不同
- 可靠消息方案中,发起方需要保证将消息发出去,并且将消息发到参与方,消息的可靠性关键由发起方来保证
- 最大努力通知方案中,发起方尽最大努力将业务处理结果通知给参与方,但参与者是可能接收不到消息,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,消息通知的可靠性关键在参与方
2)两者的业务应用场景不同
- 可靠消息方案 关注的是整体业务处理的事务一致,以异步的方式完成整个业务处理,通常是内部系统之间的调用
- 最大努力通知 关注的是业务处理后的通知事务,即将业务处理结果可靠的通知出去
3)技术解决方向不同
- 可靠消息方案 要解决消息从发出到接收的一致性,即消息发出并且被接收到
- 最大努力通知方案 无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,尽最大努力将消息通知给参与方,当消息无法被参与方接收时,由参与方主动查询消息(业务处理结果)
七、补偿型事务模式
AT、TCC、Saga 都是 补偿型的事务模式,应用层面的来提供分布式事务能力,不用依赖数据库对协议的支持,完全剥离了分布式事务方案对数据库在协议支持上的要求(留意下图中 RM 已从 DB 层移至应用层)。
AT、TCC、Saga 也是两阶段处理。有资料中描述 AT、TCC、Saga 属于 2PC 变种,也有一些文章中用 2PC 来代指 XA,在阅读资料时需根据上下文来辨识。