目录
- 一、概要
- 1. 分布式事务的概念
- 2. 分布式事务解决方案分类
- 二、常见的分布式事务解决方案
- 1. 基础的 2PC(二阶段提交)
- 1.1 核心思想
- 1.2 简介
- 1.3 主要特点
- 1.3.1 优点
- 1.3.2 缺点
- 2. 基础的 3PC(三阶段提交)
- 2.1 核心思想
- 2.2 简介
- 2.3 主要特点
- 2.3.1 优点
- 2.3.2 缺点
- 3. Seata - XA
- 3.1 核心思想
- 3.2 简介
- 3.3 主要特点
- 3.3.1 优点
- 3.3.2 缺点
- 3.3.3 适用场景
- 4. Seata - AT
- 4.1 核心思想
- 4.2 简介
- 4.3 主要特点
- 4.3.1 优点
- 4.3.2 缺点
- 4.3.3 适用场景
- 5. Seata - TCC
- 5.1 核心思想
- 5.2 简介
- 5.3 主要特点
- 5.3.1 优点
- 5.3.2 缺点
- 5.3.3 适用场景
- 6. Seata - SAGA
- 6.1 核心思想
- 6.2 简介
- 6.3 主要特点
- 6.3.1 优点
- 6.3.2 缺点
- 6.3.3 适用场景
- 7. 本地消息表
- 7.1 核心思想
- 7.2 简介
- 7.3 主要特点
- 7.3.1 优点
- 7.3.2 缺点
- 7.3.3 适用场景
- 8. RocketMQ 事务消息
- 8.1 核心思想
- 8.2 简介
- 8.3 主要特点
- 8.3.1 优点
- 8.3.2 缺点
- 8.3.3 适用场景
- 9. 最大努力通知
- 9.1 核心思想
- 9.2 简介
- 9.3 主要特点
- 9.3.1 优点
- 9.3.2 缺点
- 9.3.3 适用场景
- 三、总结
- 1. 分布式事务解决方案的对比
- 2. 使用分布式事务中间件的弊端
一、概要
1. 分布式事务的概念
官话: 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成成功,要么全部失败。
白话: 本质上来说,分布式事务就是为了保证分散在各处的数据之间的一致性。
2. 分布式事务解决方案分类
分布式是否要考虑的第一个问题 —— 是否允许回滚:
- 若不允许回滚,则事务必须执行成功,不得执行失败,因此只能采用异步通知型策略,且需要设计重试与幂等
- 若允许回滚,则需要考虑第二个问题 —— 是否要加锁:
- 若要加锁,则事务为刚性事务(CP - 强一致性)
- 若不加锁,则事务为柔性事务(AP - 最终一致性),且只能采用同步补偿策略
可以通过图表的方式更加清晰地划分各种不同的分布式事务解决方案和它们的设计思想:
接下来开始详细讲解这 9 种不同分布式事务解决方案的设计思想和优缺点。
二、常见的分布式事务解决方案
1. 基础的 2PC(二阶段提交)
1.1 核心思想
对分布式事务最暴力的做法就是让所有参与者直接执行并提交本地事务,也就是一阶段提交(1PC)。
但是这样会导致事务失去回滚能力,各个参与者之间无法进行协同,极可能出现数据不一致的问题。
当然,也可以选择放弃事务的回滚能力,要求事务必须执行成功,此时即为异步通知型事务,也即某种意义上的 1PC 模式。
为了给予分布式事务中的各个参与者协调与回滚的能力,自然地提出二阶段提交的方案:
各个参与者先执行而不提交,若所有参与者都执行成功则提交,若有某个参与者执行失败则其他参与者随之回滚。
为了让各个参与者(资源管理器)之间可以做到同步,需要一个中心节点进行统一调度,也就是事务管理器。
1.2 简介
- Prepare 阶段:
事务管理器给每个资源管理器发送 prepare 消息,资源管理器判断是否可以执行事务。
若可以执行,则开启本地事务,将事务写入本地的 redo log 和 undo log,但是不提交,然后返回成功;
若不可以执行,则返回失败。 - Commit 阶段:
若事务管理器没有收到任何资源管理器的失败反馈,则给所有资源管理器发送 commit 消息;
若事务管理器收到了部分资源管理器的失败反馈,则给所有资源管理器发送 rollback 消息。
资源管理器根据收到的消息执行本地事务的 commit 或 rollback。
1.3 主要特点
1.3.1 优点
- 灵活性:相比于不允许回滚的分布式事务解决方案,能够允许事务的执行失败,因而灵活度更高。
1.3.2 缺点
- 并发性能: 很低,在提交前,数据库中的记录是被排它锁独占的,因此并发性能会很低。
- 稳定性: 如果事务管理器发送故障,RM 无法收到 TM 下达的提交命令,则未提交的数据就会长时间被锁。而此时,其他需要访问被锁数据的线程就会因无法访问而进入阻塞,随着阻塞线程越来越多,系统可能会崩溃。
核心的问题就在于对提交前数据的加锁,既导致了并发性能低的问题,也导致了线程阻塞的问题。
能不能不加锁?或能不能减少加锁时间?大部分允许回滚的改进方案都是基于这两个方面进行设计的。
2. 基础的 3PC(三阶段提交)
2.1 核心思想
3PC 模式试图通过减少加锁时间改进 2PC 模式。
为了解决 2PC 模式存在的问题,一个自然的想法就是引入超时机制,即资源管理器设等待超时时间,等待过久就自动提交或回滚,从而防止资源长期被锁导致的并发问题和线程阻塞问题。
然而,关键的问题在于在等待超时后 RM 该如何进行决策?是应该默认提交还是默认回滚?
如果不能做出可靠的决策,直接给 2PC 添加超时机制的简单做法极有可能导致数据不一致的问题。
为此,考虑在 2PC 模式的 prepare 阶段前加一个阶段,用于帮助超时后的默认操作进行决策。
2.2 简介
- CanCommit 阶段(新增阶段):
事务管理器询问每个资源管理器是否可以执行事务。
若可以执行,则资源管理器返回 Yes;若不可以执行,则资源管理器返回 No。 - PreCommit 阶段(2PC 的 Prepare 阶段):
若事务管理器收到的所有回复均为 Yes,则向所有资源管理器发送 PreCommit 消息;
若事务管理器收到的部分回复为 No,则向所有资源管理器发送 Abort 消息。
若资源管理器收到 PreCommit 消息,则执行事务但不提交;若资源管理器收到 Abort 消息,则中断事务。
若某 RM 等待超时,则选择中断本地事务。分如下两种情况讨论。
- 实际上 TM 发送了 PreCommit 消息,但是该 RM 已中断,其他 RM 也无法执行 DoCommit
阶段,也就无法提交本地事务,因此全局事务失败,不会发生数据不一致。- 实际上 TM 发送了 Abort 消息,则该 RM 执行的中断是正确的操作,全局事务本应失败,保证了数据一致。
- DoCommit 阶段(2PC 的 Commit 阶段):
若事务管理器收到了所有资源管理器在 PreCommit 阶段返回的成功 ack,则向它们发送 doCommit 消息;
若事务管理器收到了某个资源管理器在 PreCommit 阶段返回的失败 ack,则向它们发送 rollback 消息。
资源管理器根据收到的消息执行本地事务的 commit 或 rollback。
若某 RM 等待超时,则选择提交本地事务。因为全局事务可以执行到 DoCommit 阶段,是因为在 CanCommit 阶段所有 RM 都回应了 Yes(可以执行),因此可以认为全局事务大概率会执行成功,所以选择提交本地事务,可以大概率保证数据一致性。
2.3 主要特点
2.3.1 优点
- 并发性能: 优于 2PC 模式,有效缓解了 2PC 模式中由于资源长期被锁而产生的并发问题和线程阻塞问题。
2.3.2 缺点
- 数据一致性: 不能绝对保证,3PC 模式通过添加一个 CanCommit 阶段来判断在 DoCommit 阶段等待超时的情况下应该如何决策。虽然相比于直接引入超时机制的 2PC 模式,能够更大概率地保证数据一致性,但是并不能绝对地保证数据一致性。
3. Seata - XA
3.1 核心思想
直接基于数据库的 XA 协议来忠实的实现 2PC(二阶段提交)。
3.2 简介
- 第一阶段(prepare): 所有 RM 准备执行事务并锁住所需的资源,然后各个 RM 向 TM 报告是否 ready。
- 第二阶段(commit 或 rollback): 如果所有 RM 都是 ready ,则 TM 向所有参与者发送 commit 命令;如果有参与者 RM 不是 ready,则 TM 向所有参与者发送 rollback 命令。
3.3 主要特点
3.3.1 优点
- 使用难度: 非常简单,因为是基于数据库自带特性实现,无需改动任何数据库表,也无需开发任何额外代码。
- 数据一致性: 强一致性,不会存在数据不一致的中间状态,因为是 CP 模式,放弃了可用性 A。
3.3.2 缺点
- 并发性能: 很低,因为会长时间锁资源,使其他线程被阻塞,而且各个 RM 之间会产生短板效应,所有 RM 都要锁资源并等待最慢的 RM。
3.3.3 适用场景
并发量低,对数据的一致性要求很高,对中间结果较敏感的业务场景。
4. Seata - AT
4.1 核心思想
AT 是 Automatic Transaction 的缩写,因为它能够自动完成事务的提交与回滚操作,不需要额外的代码开发。
其实现自动提交与回滚的方式如下:
TM 和 RM 所有数据库里都要额外加一张表 UNDO_LOG,自动生成并存储所有 SQL 的逆向 SQL 语句。
收到 TC 的分支提交,就删除 UNDO_LOG;收到 TC 的分支回滚,就执行 UNDO_LOG 中存储的逆向 SQL 语句。
本质上,Seata - AT 是通过补偿的方式回滚事务(同步补偿型),从而避免数据的加锁。
4.2 简介
执行本地业务和保存 redo/undo log 在同一个本地事务中,从而保证事务最终无论是 commit 还是 rollback 都必定能够执行。
第一阶段执行全部主要逻辑,第二阶段主要做回滚或日志清理。
仍然属于二阶段提交,只是以数据库加表为代价,自己模拟 MySQL 的 undo log 用于事务的回滚,从而避免加锁。
4.3 主要特点
4.3.1 优点
- 并发性能: 较高,因为本质上是同步补偿型事务,所以不用长时间锁定资源。
- 使用难度: 简单,因为是靠 Seata 自己解析生成反向 SQL 并回滚,无需额外的代码开发,只需添加数据库表。
4.3.2 缺点
- 数据一致性: 会存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性,所以若事务需要回滚,则在补偿前会存在数据不一致的中间状态。
- 业务侵入度: 少量,因为所有数据库都需要额外添加一个 UNDO_LOG 表。
4.3.3 适用场景
高并发,允许数据出现短时不一致的业务场景。
5. Seata - TCC
5.1 核心思想
TCC 是 Try、Confirm、Cancel 三个单子的首字母缩写,本质上是二阶段模式(Confirm、Cancel 在二阶段二选一)
设计理念上希望 90% 的事在 T 阶段(第一阶段)做完,CC 阶段(第二阶段)只做简单操作。
然而,TCC 模式可能需要改动数据库表,还需要自己额外编写 3 个接口,以实现其复杂逻辑。
5.2 简介
- Try 阶段: 尝试执行,完成所有业务检查,预留所需的业务资源。
- Confirm 阶段: 确认执行,真正执行业务,不需要再进行业务检查,直接使用 Try 阶段预留的业务资源(Confirm 失败后需要进行重试,因此 Confirm 操作要有幂等设计)。
- Cancel 阶段: 取消执行,释放 Try 阶段预留的业务资源(Cancel 操作也要有幂等设计)。
5.3 主要特点
5.3.1 优点
- 并发性能: 高,因为在数据库中额外存储准备状态的数据,不用长时间锁定资源。
- 灵活性: 高,各个分布式节点可以混用各种不同数据库,因为 Try、Confirm 和 Cancel 操作的具体逻辑都是自己实现。
5.3.2 缺点
- 数据一致性: 存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性。
- 使用难度: 非常困难,TCC 模式中,Seata 只负责全局事务的提交与回滚指令,具体的提交与回滚操作全部需要开发人员自己实现,从而产生大量额外工作。
- 业务侵入度: 高,因为可能涉及到数据库表的改动,自定义的 Confirm 和 Cancel 操作也容易对业务逻辑造成影响。
5.3.3 适用场景
高并发,允许数据出现短时不一致的业务场景。
6. Seata - SAGA
6.1 核心思想
考虑需要与外部第三方进行交互的事务,例如:调用支付宝支付接口 -> 出库失败 -> 调用支付宝退款接口
此时无法对外部系统的内部进行操作,只能通过接口调用进行交互,因此给每个操作实现一个逆向操作,这样就可以通过调用第三方的逆向操作接口来进行回滚。
6.2 简介
给每个操作实现一个逆向操作,通过逆向操作接口来进行回滚。
将长事务拆分为多个本地短事务,由事务管理器协调。如果某个步骤失败,则根据相反的顺序依次调用补偿操作。
本质上是同步补偿型的二阶段模式。
6.3 主要特点
6.3.1 优点
- 并发性能: 较高,因为无需对资源加锁,而是直接提交本地短事务,若需回滚则执行逆向操作。
6.3.2 缺点
- 数据一致性: 存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性。
- 使用难度: 困难,因为本地短事务的提交操作和逆向补偿操作都需要开发者自己实现。
6.3.3 适用场景
需要与外部第三方进行交互,只能通过接口调用进行事务的回滚操作的场景。
异步通知型方案:可靠消息 VS 最大努力通知
都是异步通知型,区别在于消息的发送方和接收方谁来保证消息必定被传达。
- 可靠消息:由发送方保证。
- 最大努力通知:由接收方保证。
可靠消息的两个关键要求:
- 事务发起方一定能够将消息成功发送出去(让事务发起方的本地事务与能够消息协同)
- 事务参与方一定能够成功接收到消息(让事务参与方的本地事务与能够消息协同) 为了满足可靠消息的两个要求,必须有一个节点负责落库并维护消息, 由事务发起方维护即为本地消息表,由消息中间件维护即为 MQ 事务消息。
7. 本地消息表
7.1 核心思想
通过让事务发起方落库并维护消息的方式,满足可靠消息模式的两个关键要求。
- 要求 1: 将本地消息表的落库与发起方的本地事务放入同一个本地事务中,从而保证发起方本地事务的执行成功与本地消息的落库成功保持原子性。
- 要求 2: 本地消息表维护消息的状态,对于“未完成”状态的消息,不断重试发送,直到收到参与方对该消息的回执,从而保证消息必定能够被参与方成功接收。
7.2 简介
- 写业务数据和写消息数据在一个本地事务中,从而保证了业务操作和发送消息的原子性。
- 生产者定时扫描本地消息表,将未删除的消息进行重发,从而保证所有消息都会发送给 MQ。
- 生产者收到 MQ 的回执 ACK 之后删除本地消息表中的对应消息,从而保证所有消息都被 MQ 成功接收。
- MQ 会将未能成功发送给消费者的消息重新发送,从而保证所有消息都会发送给消费者。
7.3 主要特点
7.3.1 优点
- 灵活性: 不依赖任何分布式事务中间件,完全依靠开发者自己实现的可靠消息模式。
7.3.2 缺点
- 使用难度: 中等,需要在数据库中添加本地消息表,并实现其增删改查和定时扫码重发、回执接收等功能。
- 数据一致性: 存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性。
- 业务入侵度: 较高,容易与业务耦合,不利于扩展和维护,难以实现通用。
7.3.3 适用场景
可以接受异步通知型方案,不希望依赖分布式事务中间件的业务场景。
8. RocketMQ 事务消息
8.1 核心思想
通过让消息中间件落库并维护消息的方式,满足可靠消息模式的两个关键要求。
- 要求 1: 发起方的本地事务执行前 MQ 落库半消息,发起方的本地事务执行成功则 MQ 将半消息投递到参与方,发起方的本地事务执行失败则 MQ 将半消息丢弃,从而保证发起方本地事务的执行成功与本地消息的发送成功保持原子性。
- 要求 2: 由消息中间件对“未完成”的消息进行重试,直到收到参与方的回执,从而保证消息必定能够被参与方成功接收。
8.2 简介
- MQ 定期回查每个未 commit 或 rollback 的半消息对应的生产者的事务状态,从而保证生产者方的本地事务必有一个终态。
- 消费者完成本地事务后需要将 MQ 中对应的消息标记为已消费,从而保证消费者必定完成每个消息对应的事务。
因此,所以使用 RocketMQ 的事务消息需要自己实现两个方法:
- 执行本地事务的方法:在发起方的本地事务的末尾需要给 MQ 发 commit / rollback 的消息。
- 查询本地事务状态的方法:提供给 MQ,用于对发起方的本地事务状态进行回查。
8.3 主要特点
8.3.1 优点
- 业务侵入度: 业务系统与消息系统的耦合度明显低于本地消息表模式。
- 使用难度: 比较简单,只需实现本地事务的执行与查询两个方法,消息的维护由中间件负责。
8.3.2 缺点
- 灵活性: 需要依赖消息中间件。
- 数据一致性: 存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性。
8.3.3 适用场景
可以接受异步通知型方案,对达到最终一致性的时间敏感度较低的业务场景。
9. 最大努力通知
9.1 核心思想
与可靠消息模式相反,最大努力通知模式由接收方保证消息必定被传达。
事务发起方只负责“尽最大努力”通知事务参与方,但是并不保证消息的成功送达,事务参与方需要主动调用消息校对接口来确保事务主动方的消息被成功接收。
9.2 简介
9.3 主要特点
9.3.1 优点
- 业务侵入度: 较低,事务主动方只需提供一个消息校对接口。
9.3.2 缺点
- 数据一致性: 存在数据不一致的中间状态,因为是保障最终一致性的柔性事务(AP),放弃了数据的强一致性。
- 使用难度: 中等,需要额外的定期校验机制对数据进行兜底,保证数据的最终一致性。
9.3.3 适用场景
跨平台、跨企业的系统间业务交互,外部系统网络环境复杂、不可信,对达到最终一致性的时间敏感度较低的业务场景。如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景。
三、总结
1. 分布式事务解决方案的对比
基础 2PC | 基础 3PC | Seata - XA | Seata - AT | Seata - TCC | Seata - SAGA | 本地消息表 | 事务消息 | 最大努力通知 | |
---|---|---|---|---|---|---|---|---|---|
一致性 | 强一致 | 强一致 | 强一致 | 最终一致 | 最终一致 | 最终一致 | 最终一致 | 最终一致 | 最终一致 |
并发性能 | 很低 | 较低 | 很低 | 较高 | 高 | 较高 | 中等 | 中等 | 较高 |
使用难度 | 简单 | 中等 | 简单 | 简单 | 非常困难 | 困难 | 中等 | 比较简单 | 中等 |
业务侵入 | 无侵入 | 无侵入 | 无侵入 | 少量 | 高 | 较高 | 较高 | 较低 | 较低 |
优点 | · 允许事务的执行失败和回滚,因而灵活度更高 | · 业务无侵入 · 并发性能高于 2PC | · 使用简单 · 强一致性,无数据不一致的中间状态 | · 使用简单 · 并发性能较高 | · 并发性能较高 · 可以混用数据库 | · 并发性能较高 · 便于与外部第三方进行交互 | · 易于实现,使用简单 · 不依赖任何分布式事务中间件 | · 对业务侵入较少 · 通过消息中间件解耦,下游事务异步化 | · 对业务侵入较少 |
缺点 | · 稳定性差,容易造成大量线程阻塞 | · 无法绝对保证数据一致性 | · 并发性能很低,会长时间锁资源 | · 最终一致性,会存在数据不一致的中间状态 · 有少量的业务侵入性 | · 最终一致性,会存在数据不一致的中间状态 · 使用难度高,会带来大量额外工作 · 业务侵入度高 | · 最终一致性,会存在数据不一致的中间状态 · 使用难度较高,会带来额外的开发工作 | · 最终一致性,会存在数据不一致的中间状态 · 容易与业务耦合,难以实现通用 | · 最终一致性,会存在数据不一致的中间状态 · 需要依赖消息中间件。 | · 最终一致性,会存在数据不一致的中间状态 · 需要额外的定期校验机制对数据进行兜底 |
适用业务 | · 要求强一致性 · 短事务 · 并发量低 | · 要求强一致性 · 短事务 · 并发量低 | · 要求强一致性 · 短事务 · 并发量低 | · 并发量高 · 允许出现短时间的数据不一致 | · 并发量高 · 允许出现短时间的数据不一致 | · 并发量高 · 允许出现短时间的数据不一致 · 需要与外部第三方进行交互 | · 可以接受异步通知型方案 · 允许出现短时间的数据不一致 · 不希望依赖分布式事务中间件 | · 可以接受异步通知型方案 · 允许出现短时间的数据不一致 | · 允许出现短时间的数据不一致 · 跨平台、跨企业的系统间业务交互 · 外部系统网络环境复杂、不可信 |
2. 使用分布式事务中间件的弊端
- 引入分布式事务中间件会造成系统大规模耦合。
- TC(事务协调者)作为基础设施却需要对外部的 TM(事务管理器)暴露,一定程度上违背了微服务的理念。
- 带来了额外的学习成本与开发成本,提高了开发者的使用门槛,如果用不好反而会带来更多棘手的问题。