分布式事务
分布式事务详解
目录
一、分布式事务背景
1.1 从单体到分布式
在传统的单体架构中,应用的所有模块运行在同一个进程中,共享同一个数据库实例。数据库事务(ACID)天然保证了一致性:
┌────────────────────────────────┐
│ 单体应用 │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │订单模块│ │库存模块│ │支付模块│ │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ └────────┼────────┘ │
│ ┌────▼────┐ │
│ │ 单一数据库 │ │
│ └─────────┘ │
└────────────────────────────────┘
随着业务规模增长,单体架构暴露出大量问题:代码耦合严重、扩展困难、部署周期长、故障隔离性差。于是微服务/分布式架构应运而生:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 库存服务 │ │ 支付服务 │
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │订单DB │ │ │ │库存DB │ │ │ │支付DB │ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────────┘ └──────────┘ └──────────┘
1.2 分布式事务的核心问题
拆分后,每个服务拥有独立的数据库,一个跨服务的业务操作(如"下单")需要同时操作多个数据库,无法再使用单机数据库事务来保证一致性。
典型场景:电商下单
下单流程:
1. 订单服务 → 创建订单记录(订单数据库)
2. 库存服务 → 扣减商品库存(库存数据库)
3. 支付服务 → 扣减用户余额(支付数据库)
要求:三者要么全部成功,要么全部回滚
如果第 1、2 步成功,第 3 步失败,订单和库存已经变更,但支付未完成,数据就处于不一致状态。
1.3 核心理论基石
CAP 定理
由 Eric Brewer 在 2000 年提出,分布式系统无法同时满足以下三点,最多只能满足其中两点:
| 属性 | 含义 | 放弃的后果 |
|---|---|---|
| C(Consistency)一致性 | 所有节点在同一时刻看到相同的数据 | 放弃 C:允许读取到旧数据(最终一致性) |
| A(Availability)可用性 | 每个请求都能获得非错误的响应 | 放弃 A:部分请求可能被阻塞或超时 |
| P(Partition Tolerance)分区容错性 | 系统在出现网络分区时仍能正常工作 | 放弃 P:网络分区时系统停止服务(不现实) |
关键结论: 在分布式系统中,网络分区是不可避免的,因此 P 必须满足。实际选择是 CP 或 AP:
- CP 系统:发生分区时,牺牲可用性,保证一致性(如 2PC、ZooKeeper)
- AP 系统:发生分区时,牺牲一致性,保证可用性(如 Saga、TCC 的最终一致性)
BASE 理论
BASE 是 CAP 定理中 AP 方案的实践指导,是对 ACID 的弱化:
| 属性 | 含义 |
|---|---|
| BA(Basically Available)基本可用 | 系统出现故障时允许损失部分可用性,但核心功能仍然可用 |
| S(Soft State)软状态 | 允许系统存在中间状态,该中间状态不影响系统整体可用性 |
| E(Eventually Consistent)最终一致性 | 系统中的所有数据副本经过一段时间后,最终达到一致状态 |
ACID 与 BASE 对比如下:
| 维度 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致性 | 最终一致性 |
| 隔离性 | 强隔离 | 弱隔离 |
| 可用性 | 低 | 高 |
| 适用场景 | 金融、银行等强一致性需求 | 互联网电商、社交等高并发场景 |
二、2PC(两阶段提交)
2.1 概述
2PC(Two-Phase Commit,两阶段提交)是最经典的分布式事务协议,由协调者(Coordinator)和多个参与者(Cohort/Participant)组成。顾名思义,它把事务提交过程分为两个阶段:准备阶段和提交阶段。
2.2 角色定义
| 角色 | 职责 |
|---|---|
| 协调者(Coordinator) | 事务管理器,负责发起、协调和决策事务的提交或回滚 |
| 参与者(Participant) | 资源管理器(如数据库),负责执行事务的本地操作 |
2.3 流程详解
阶段一:准备阶段(Prepare Phase / Voting Phase)
协调者 参与者
│ │
│──── ① 发送 Prepare 请求 ────────►│
│ │ ② 执行本地事务(写 undo/redo 日志)
│ │ 但不提交
│ │ ③ 锁定相关资源
│◄─── ④ 回复 YES(可提交)or NO(失败)──│
│ │
协调者做的事情:
- 向所有参与者发送 Prepare 消息
- 等待所有参与者的响应
参与者做的事情:
- 收到 Prepare 请求后,执行本地事务操作,记录 undo(回滚日志)和 redo(重做日志)
- 事务未提交,资源处于锁定状态
- 回复协调者:如果本地事务执行成功,回复 “YES”(可以提交);如果执行失败,回复 “NO”(需要回滚)
阶段二:提交阶段(Commit Phase)
情况 A:所有参与者都回复 YES(提交)
协调者 参与者
│ │
│──── ① 发送 Commit 请求 ────────►│
│ │ ② 提交本地事务
│ │ ③ 释放锁资源
│◄─── ④ 回复 ACK(确认)────────│
│ │
情况 B:任一参与者回复 NO 或超时(回滚)
协调者 参与者
│ │
│──── ① 发送 Rollback 请求 ──────►│
│ │ ② 使用 undo 日志回滚
│ │ ③ 释放锁资源
│◄─── ④ 回复 ACK(确认)────────│
│ │
2.4 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 原理简单 | 只需要两个阶段,逻辑清晰,易于理解和实现 |
| 强一致性 | 保证所有参与者要么全部提交,要么全部回滚(原子性) |
| 行业标准 | 有 XA 规范支撑,多数数据库(MySQL、Oracle、PostgreSQL)都支持 XA 事务 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 同步阻塞 | 参与者在准备阶段锁定资源后,必须等待协调者指令才能继续,期间资源被锁定,其他事务无法访问,并发性能极差 |
| 单点故障 | 协调者是整个系统的单点,一旦协调者宕机,所有参与者将无限期阻塞,整个系统瘫痪 |
| 数据不一致风险 | 提交阶段中,如果协调者只向部分参与者发送了 Commit 指令后宕机,部分参与者提交了、部分未提交,数据出现不一致 |
| 网络开销大 | 每个事务需要多次网络往返,延迟较高 |
| 不适合高并发 | 因为资源锁定时间长,不适合高并发互联网场景 |
| 容错性差 | 对网络分区和节点故障的容忍能力有限 |
2.5 当前主流采用情况
2PC/XA 在互联网主流场景中已基本被淘汰,原因如下:
- 互联网场景追求高并发、低延迟,2PC 的资源锁定和同步阻塞完全不可接受
- 微服务架构下,服务之间通过 REST/RPC 通信,而非直接使用数据库 XA 协议
- 长事务场景下锁竞争会导致系统雪崩
仍有小范围使用的场景:
- 传统企业应用(银行核心系统、ERP 等强一致性需求)
- 跨数据库操作(同一应用内跨多个数据库实例)
- 金融机构的某些合规场景
三、3PC(三阶段提交)
3.1 概述
3PC(Three-Phase Commit,三阶段提交)是 2PC 的改进版本,在 2PC 的两个阶段之间插入了一个预提交阶段(Pre-Commit),旨在解决 2PC 的同步阻塞和单点故障问题。同时引入了超时机制,允许参与者在协调者长时间无响应时自行决策。
3.2 流程详解
阶段一:CanCommit(询问阶段)
协调者 参与者
│ │
│──── ① 发送 CanCommit 请求 ──────►│
│ │ ② 判断是否可以执行事务
│ │ (不执行事务,仅做可行性判断)
│◄─── ③ 回复 YES 或 NO ──────────│
│ │
协调者: 向所有参与者发送 CanCommit 请求,询问是否可以执行事务
参与者: 根据自身状态判断能否执行事务(如检查网络状况、资源可用性等),不执行任何事务操作,回复 YES 或 NO
异常处理: 如果任何参与者回复 NO 或超时,协调者直接终止事务
阶段二:PreCommit(预提交阶段)
情况 A:所有参与者回复 YES
协调者 参与者
│ │
│──── ① 发送 PreCommit 请求 ──────►│
│ │ ② 执行本地事务(写 undo/redo 日志)
│ │ 但不提交
│ │ ③ 锁定相关资源
│◄─── ④ 回复 ACK 或 NO ──────────│
│ │
情况 B:有参与者回复 NO 或超时
协调者 参与者
│ │
│──── ① 发送 Abort(中断)请求 ───►│
│ │ ② 回滚(或无需操作)
│◄─── ③ 回复 ACK ────────────────│
│ │
阶段三:DoCommit(提交阶段)
情况 A:协调者收到所有 PreCommit 的 ACK
协调者 参与者
│ │
│──── ① 发送 DoCommit 请求 ──────►│
│ │ ② 提交本地事务
│ │ ③ 释放锁资源
│◄─── ④ 回复 ACK ────────────────│
│ │
情况 B:协调者未收到所有 ACK 或超时
协调者 参与者
│ │
│──── ① 发送 Rollback 请求 ──────►│
│ │ ② 使用 undo 日志回滚
│ │ ③ 释放锁资源
│◄─── ④ 回复 ACK ────────────────│
│ │
3.3 核心改进:参与者的超时中断机制
3PC 相比 2PC 最大的改进是:在 DoCommit 阶段,如果参与者长时间没有收到协调者的指令,它不会无限期阻塞,而是会自动提交事务。
参与者超时后的决策逻辑:
在 PreCommit 阶段超时:
→ 参与者不确定协调者是否存活,也不确定其他参与者的状态
→ 安全策略:回滚事务(因为可能还没进入 DoCommit 阶段)
在 DoCommit 阶段超时:
→ 参与者已经收到了 PreCommit 指令,说明协调者已经决定提交
→ 安全策略:自动提交事务(因为其他参与者大概率也收到了 PreCommit)
这个机制解决了 2PC 中协调者宕机后参与者无限阻塞的问题。
3.4 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 减少阻塞 | 引入了超时机制,参与者在 DoCommit 阶段超时后可自动提交,不会无限期阻塞 |
| 增加容错性 | 协调者故障后,参与者可以在超时后自行决策,降低了单点故障的影响 |
| PreCommit 缓冲 | 在准备阶段前增加了 CanCommit 询问阶段,可以提前发现不可用的参与者,减少不必要的资源锁定 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 实现复杂 | 比 2PC 多了一个阶段,增加了代码复杂度和调试难度 |
| 网络开销更大 | 三个阶段需要至少三次网络往返,延迟比 2PC 更高 |
| 数据不一致风险仍存在 | 在网络分区场景下,如果部分参与者收到 PreCommit 后与协调者失联,它们超时后自动提交;但协调者可能因为未收到 ACK 而发送回滚指令,导致部分提交、部分回滚的不一致 |
| 性能差 | 相比 2PC,多了一个阶段,延迟更高;资源锁定时间仍然较长 |
| 假设不可靠 | "进入 DoCommit 阶段就自动提交"的假设在网络分区场景下可能不成立,协调者可能决定回滚 |
3.5 当前主流采用情况
3PC 在实际生产中极少被采用,原因如下:
- 虽然理论上解决了 2PC 的阻塞问题,但引入了新的数据不一致风险(网络分区时自动提交可能导致数据不一致)
- 增加了网络开销,性能更差
- 实现复杂度高,而收益有限
- 在互联网场景中,人们更倾向于使用最终一致性的方案(如 Saga、TCC)
3PC 更多是学术和研究价值,作为分布式一致性协议的理论演进路径的一部分,后来被 Paxos/Raft 等共识算法所取代。
四、TCC(Try-Confirm-Cancel)
4.1 概述
TCC(Try-Confirm-Cancel)是一种补偿型的分布式事务方案,由 Pat Helland 在 2007 年提出。TCC 的核心思想是:将每个服务的操作拆分为三个阶段,通过预留资源 + 确认 / 取消来实现最终一致性。
TCC 属于最终一致性方案,不要求资源锁定,而是通过"预留"和"补偿"来保证数据最终一致。
4.2 三个阶段
阶段一:Try(尝试/预留)
Try 阶段:对所有服务进行资源检查和预留
- 订单服务:将订单状态设置为"预创建",冻结库存
- 库存服务:检查库存是否充足,扣减冻结库存(但不减少实际可售库存)
- 支付服务:检查余额是否充足,冻结对应金额
Try 阶段的核心要求:
- 完成所有业务检查(一致性检查)
- 预留必须的业务资源,但不实际执行核心操作
- 预留的资源应保证后续 Confirm 阶段能够成功执行
阶段二:Confirm(确认)
Confirm 阶段:所有 Try 成功后,执行真正的业务操作
- 订单服务:将订单状态从"预创建"改为"已创建"
- 库存服务:从冻结库存中扣除,更新实际库存
- 支付服务:从冻结金额中扣除,更新实际余额
Confirm 阶段的核心要求:
- 必须幂等,允许重试
- 如果没有异常,Confirm 一定会成功(因为 Try 阶段已经预留了资源)
- 如果 Confirm 失败,需要不断重试直到成功
阶段三:Cancel(取消/补偿)
Cancel 阶段:任一 Try 失败时,对所有已 Try 成功的服务进行补偿
- 订单服务:将订单状态从"预创建"改为"已取消"
- 库存服务:释放冻结库存,恢复可售库存
- 支付服务:释放冻结金额,恢复可用余额
Cancel 阶段的核心要求:
- 必须幂等,允许重试
- 释放 Try 阶段预留的资源
- 如果 Cancel 失败,需要不断重试直到成功
4.3 流程示例
以电商下单为例,展示 TCC 的完整流程:
场景:用户下单购买商品,价格为 100 元
┌─────────────────────────────────────────────────────┐
│ TCC 协调者 │
└──┬──────────────────┬──────────────────┬────────────┘
│ │ │
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│订单服务│ │库存服务│ │支付服务│
└──────┘ └──────┘ └──────┘
===== Try 阶段 =====
订单服务: 创建订单(状态=预创建),冻结商品
库存服务: 检查库存>=1,冻结库存 -1
支付服务: 检查余额>=100,冻结余额 100
===== 全部 Try 成功 → Confirm 阶段 =====
订单服务: 订单状态:预创建 → 已创建
库存服务: 冻结库存扣减生效,实际库存 -1
支付服务: 冻结余额扣减生效,实际余额 -100
===== 任一步 Try 失败 → Cancel 阶段 =====
假设:支付服务 Try 失败(余额不足)
订单服务: 取消订单(状态=预创建 → 已取消),释放冻结
库存服务: 释放冻结库存,冻结库存 +1
支付服务: 无需操作(Try 阶段未成功)
4.4 补偿机制(重点)
TCC 的补偿机制是其核心所在,与 2PC 的 undo 日志回滚不同,TCC 的补偿是业务层面的,需要开发者自己实现。
补偿策略
┌────────────────────────────────────────────────┐
│ TCC 补偿机制 │
├────────────────────────────────────────────────┤
│ │
│ 1. 空回滚(Null Rollback) │
│ - 场景:Try 请求未到达服务或超时,协调者 │
│ 直接发送了 Cancel 指令 │
│ - 处理:Cancel 接口需要识别到没有对应的 Try │
│ 记录,直接返回成功(空回滚) │
│ - 实现:通常在数据库记录 Try 操作日志, │
│ Cancel 时检查日志是否存在 │
│ │
│ 2. 防悬挂(Prevent Suspension) │
│ - 场景:Cancel 先于 Try 到达(网络乱序), │
│ 之后 Try 再到达 │
│ - 处理:Try 接口需要检查是否有对应的 Cancel │
│ 记录,如果有则拒绝执行 Try │
│ - 实现:Cancel 时记录操作日志,Try 时检查 │
│ │
│ 3. 幂等性(Idempotence) │
│ - Try/Confirm/Cancel 都可能被重试,必须保证 │
│ 多次调用结果一致 │
│ - 实现:基于唯一事务 ID 做去重判断 │
│ │
│ 4. 重试机制(Retry) │
│ - Confirm/Cancel 失败后需要不断重试 │
│ - 重试策略:指数退避、固定间隔等 │
│ - 人工兜底:超过最大重试次数后转为人工处理 │
│ │
└────────────────────────────────────────────────┘
补偿代码示例
// 库存服务的 TCC 实现
@Service
public class InventoryTccService {
// Try:预留库存
@Transactional
public boolean tryDeductStock(String txId, String productId, int count) {
// 防悬挂:检查是否已有 Cancel 记录
if (tccLogDao.existsCancel(txId)) {
return false; // 拒绝执行 Try
}
// 幂等:检查是否已经 Try 过
if (tccLogDao.existsTry(txId)) {
return true; // 已 Try 过,直接返回成功
}
// 业务检查
int frozenStock = productDao.getFrozenStock(productId);
int availableStock = productDao.getAvailableStock(productId);
if (availableStock < count) {
return false; // 库存不足
}
// 减少可用库存,增加冻结库存
productDao.decreaseAvailableStock(productId, count);
productDao.increaseFrozenStock(productId, count);
// 记录 Try 日志
tccLogDao.insertTryLog(txId, productId, count);
return true;
}
// Confirm:确认扣减
@Transactional
public boolean confirmDeductStock(String txId, String productId, int count) {
// 幂等检查
if (tccLogDao.existsConfirm(txId)) {
return true;
}
// 减少冻结库存,更新总库存
productDao.decreaseFrozenStock(productId, count);
productDao.decreaseTotalStock(productId, count);
// 记录 Confirm 日志
tccLogDao.insertConfirmLog(txId, productId, count);
return true;
}
// Cancel:释放冻结库存
@Transactional
public boolean cancelDeductStock(String txId, String productId, int count) {
// 空回滚:如果 Try 日志不存在,说明 Try 未执行
if (!tccLogDao.existsTry(txId)) {
// 记录 Cancel 日志(防悬挂用)
tccLogDao.insertCancelLog(txId, productId, count);
return true; // 空回滚,直接返回成功
}
// 幂等检查
if (tccLogDao.existsCancel(txId)) {
return true;
}
// 恢复可用库存,减少冻结库存
productDao.increaseAvailableStock(productId, count);
productDao.decreaseFrozenStock(productId, count);
// 记录 Cancel 日志
tccLogDao.insertCancelLog(txId, productId, count);
return true;
}
}
4.5 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 无资源锁定 | 不像 2PC 那样锁定数据库资源,Try 阶段只做"预留",不阻塞其他事务,并发性能高 |
| 最终一致性 | 通过 Confirm/Cancel 保证数据最终一致,适合高并发互联网场景 |
| 业务可控 | 由业务方自己实现 Try/Confirm/Cancel,可以灵活处理各种业务场景 |
| 性能好 | 没有全局锁,各服务可以并行执行,不依赖单一协调者进行强一致性协调 |
| 容错性好 | 通过重试机制保证 Confirm/Cancel 最终成功,单点故障影响小 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 侵入性强 | 每个参与的服务都需要实现 Try/Confirm/Cancel 三个接口,对业务代码侵入大 |
| 开发成本高 | 需要处理空回滚、防悬挂、幂等性等复杂问题,开发工作量大 |
| 实现难度大 | 业务逻辑的补偿操作往往比正向操作更复杂,容易出错 |
| 维护成本高 | 业务逻辑变更时,需要同时修改 Try/Confirm/Cancel 三个接口 |
| 不适合长事务 | 如果 Confirm 阶段耗时过长,整个事务时间会拉长,但好在没有锁 |
| 数据一致性弱 | 在 Try 和 Confirm 之间,数据处于中间状态(冻结状态),外部可能读到不一致数据 |
4.6 当前主流采用情况
TCC 是目前互联网公司中较为主流的分布式事务方案,尤其适用于以下场景:
- 金融/支付场景:对资金安全要求高,需要精确的补偿机制
- 电商核心交易链路:下单、扣库存、扣款等需要强保证的场景
- 跨服务数据一致性场景:微服务架构下需要保证最终一致性的场景
代表实践:
- 蚂蚁集团:大量使用 TCC 处理支付、转账等核心交易场景
- 美团、滴滴等:在订单、支付等核心流程中使用 TCC
- 阿里的 Seata 框架内置了 TCC 模式支持
五、Saga 模式
5.1 概述
Saga 模式最早由 Hector Garcia-Molina 在 1987 年提出,用于解决**长事务(Long-Lived Transaction)**问题。Saga 的核心思想是:将一个长事务拆分为多个本地事务,每个本地事务都有对应的补偿事务,当某个本地事务失败时,按相反顺序执行之前成功事务的补偿操作。
Saga 事务 = 一系列本地事务 + 对应的补偿事务
T1 → T2 → T3 → ... → Tn
C1 C2 C3 Cn
如果 T3 失败:
→ 执行 C2(补偿 T2)→ 执行 C1(补偿 T1)
5.2 两种协调模式
模式一:编排式(Choreography)
┌────────┐ 事件A ┌────────┐ 事件B ┌────────┐
│ 服务A │────────►│ 服务B │────────►│ 服务C │
│ (订单) │◄────────│ (库存) │◄────────│ (支付) │
└────────┘ 补偿事件 └────────┘ 补偿事件 └────────┘
特点:
- 每个服务独立监听事件并执行自己的事务
- 自己决定何时执行本地事务,并发布事件触发下一个服务
- 失败时发布补偿事件,由上游服务自己处理补偿
优点: 松耦合,服务之间不直接依赖
缺点: 流程分散在各个服务中,难以追踪和理解整体流程
模式二:编排器式(Orchestration)
┌──────────────┐
│ Saga 编排器 │
│ (Order Saga) │
└──┬───┬───┬──┘
│ │ │
┌────▼┐ ┌▼───┐ ┌────▼┐
│订单服务│ │库存服务│ │支付服务│
└──────┘ └─────┘ └──────┘
特点:
- 由一个 Saga 编排器(Orchestrator)集中管理整个流程
- 编排器按顺序调用各个服务,并处理失败补偿
- 流程逻辑集中,便于管理和追踪
优点: 流程集中管理,清晰可控,易于维护
缺点: 编排器成为关键节点,逻辑复杂度集中
目前业界以编排器式为主流,因为它更易于管理和维护。
5.3 流程示例
以电商下单为例,展示 Saga 编排器模式的完整流程:
Saga 编排器:OrderSaga
步骤 1:创建订单
→ 调用订单服务.createOrder()
→ 成功:记录补偿函数 = 订单服务.cancelOrder(orderId)
→ 失败:终止 Saga,无需补偿
步骤 2:扣减库存
→ 调用库存服务.deductStock()
→ 成功:记录补偿函数 = 库存服务.restoreStock(productId, count)
→ 失败:执行补偿步骤 1(取消订单)
步骤 3:扣减余额
→ 调用支付服务.deductBalance()
→ 成功:Saga 完成
→ 失败:执行补偿步骤 2(恢复库存)→ 执行补偿步骤 1(取消订单)
5.4 补偿机制(重点)
Saga 的补偿机制是反向补偿,即按照事务执行的逆序执行补偿操作。
补偿的核心原则
Saga 补偿的三大原则:
1. 逆序补偿(Backward Recovery)
- 从失败点开始,向前(逆序)依次执行补偿
- 例如:T1→T2→T3(失败),补偿顺序:C2→C1
2. 补偿幂等
- 补偿操作可能被重试,必须保证幂等性
- 同一个补偿操作多次执行,结果一致
3. 补偿必须成功
- 补偿操作失败后需要不断重试,直到成功
- 超过重试上限需要人工介入
补偿策略详解
┌──────────────────────────────────────────────────────┐
│ Saga 补偿策略 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. 正向补偿(Forward Recovery) │
│ - 适用场景:失败后可以重试当前步骤(非致命错误) │
│ - 处理方式:重试失败的步骤,不触发逆序补偿 │
│ - 示例:库存扣减因网络超时失败,重试可能成功 │
│ │
│ 2. 反向补偿(Backward Recovery) │
│ - 适用场景:当前步骤是业务失败,无法重试 │
│ - 处理方式:逆序执行所有成功步骤的补偿 │
│ - 示例:余额不足,无法完成支付,需要回滚整个订单 │
│ │
│ 3. 混合补偿(Mixed Recovery) │
│ - 适用场景:部分步骤可重试,部分不可重试 │
│ - 处理方式:先重试可重试的步骤,超时后转为反向补偿 │
│ │
│ 4. 隔离性处理(Isolation) │
│ - 问题:Saga 是多个独立的本地事务,中间状态 │
│ 可能被其他事务读取,导致脏读 │
│ - 解决方案: │
│ a. 语义锁:在业务字段上标记"处理中"状态 │
│ b. 版本号/乐观锁:更新时检查版本号 │
│ c. 事务间写隔离:确保冲突操作有合理的顺序 │
│ │
└──────────────────────────────────────────────────────┘
补偿代码示例
// Saga 编排器实现
@Component
public class OrderSagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Transactional
public void executeOrderSaga(OrderRequest request) {
// 记录 Saga 执行日志
SagaLog sagaLog = new SagaLog(request.getOrderId());
sagaLogDao.insert(sagaLog);
try {
// 步骤 1:创建订单
Order order = orderService.createOrder(request);
sagaLog.addStep("CREATE_ORDER", () -> {
orderService.cancelOrder(order.getId()); // 补偿函数
});
// 步骤 2:扣减库存
inventoryService.deductStock(request.getProductId(), request.getCount());
sagaLog.addStep("DEDUCT_STOCK", () -> {
inventoryService.restoreStock(request.getProductId(), request.getCount());
});
// 步骤 3:扣减余额
paymentService.deductBalance(request.getUserId(), request.getAmount());
sagaLog.addStep("DEDUCT_BALANCE", () -> {
paymentService.refundBalance(request.getUserId(), request.getAmount());
});
// 全部成功
sagaLog.setStatus(SagaStatus.COMPLETED);
sagaLogDao.update(sagaLog);
} catch (Exception e) {
// 执行补偿:逆序执行所有已记录的补偿函数
sagaLog.compensate(); // 从最后一步开始向前补偿
sagaLog.setStatus(SagaStatus.COMPENSATED);
sagaLogDao.update(sagaLog);
throw new SagaCompensationException("Saga 补偿完成", e);
}
}
}
// SagaLog 补偿逻辑
public class SagaLog {
private List<SagaStep> steps = new ArrayList<>();
public void compensate() {
// 逆序补偿
for (int i = steps.size() - 1; i >= 0; i--) {
SagaStep step = steps.get(i);
try {
// 检查是否已补偿(幂等)
if (!step.isCompensated()) {
step.getCompensation().execute(); // 执行补偿
step.setCompensated(true);
}
} catch (Exception e) {
// 补偿失败,记录日志,异步重试或人工处理
compensationLogDao.insert(new CompensationLog(step, e));
// 继续执行前一个步骤的补偿(尽力补偿)
}
}
}
}
5.5 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 高可用 | 没有全局锁,各服务独立执行,不阻塞其他事务,可用性极高 |
| 高性能 | 本地事务执行快,不需要等待其他参与者,并发性能好 |
| 适合长事务 | 不锁定资源,事务可以持续很长时间,适合工作流、审批流等场景 |
| 松耦合 | 编排式模式下,服务之间通过事件异步通信,耦合度低 |
| 容错性好 | 通过补偿机制处理失败,没有单点故障问题 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 缺乏隔离性 | Saga 的各步骤是独立的本地事务,中间状态可见,可能导致脏读、不可重复读等问题 |
| 补偿实现复杂 | 需要为每个正向操作编写对应的补偿操作,业务逻辑逆向还原往往很困难 |
| 补偿可能失败 | 补偿操作本身可能失败(如服务宕机),需要重试和人工兜底机制 |
| 无自动回滚 | 不像 2PC 那样数据库自动回滚,需要手动编写所有补偿逻辑 |
| 数据一致性弱 | 只有在所有 Saga 步骤完成后,数据才最终一致,中间可能存在不一致窗口 |
5.6 与 TCC 的对比
| 维度 | TCC | Saga |
|---|---|---|
| 资源预留 | 有(Try 阶段预留资源) | 无(直接执行) |
| 事务失败处理 | Cancel 回滚 | 逆序补偿 |
| 隔离性 | 较好(Try 预留资源) | 较差(执行即生效) |
| 实现复杂度 | 高(3 个接口) | 中(1 个正向 + 1 个补偿) |
| 代码侵入性 | 高 | 中 |
| 适用场景 | 金融、支付等强一致性需求 | 长事务、工作流、非核心业务 |
5.7 当前主流采用情况
Saga 模式是当前微服务架构中最主流的分布式事务方案之一,原因如下:
- 互联网追求高可用、高并发,Saga 的无锁特性完美契合
- 微服务架构天然适合 Saga 的编排式或编排器式模式
- 补偿逻辑虽复杂,但比 TCC 侵入性小,更易于实现
- 适合长事务场景,这是其他方案难以解决的
代表实践:
- Netflix:Conductor 工作流引擎使用 Saga 处理视频编码等长事务
- Uber:使用 Saga 处理打车订单的生命周期
- 阿里的 Seata 框架内置 Saga 模式支持
- 各类微服务框架(如 Spring Cloud、Axon Framework)都提供了 Saga 支持
六、AT 模式
6.1 概述
AT 模式(Automatic Transaction,自动事务)是阿里 Seata 框架独创的分布式事务模式,它是对 2PC 的改进和优化。AT 模式的核心思想是:基于关系型数据库的本地事务,通过自动生成 undo 日志(前镜像和后镜像)实现自动回滚,对业务代码零侵入。
AT 模式本质上是一种改良版的 2PC,它解决了 2PC 的以下问题:
- 不需要 XA 协议支持,基于普通本地事务
- 锁粒度更细(全局锁而不是数据库锁)
- 只在第一阶段短暂的锁定资源
6.2 架构角色
┌──────────────────────────────────────────────────────┐
│ Seata 架构 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ TC(事务 │ │ TM(事务 │ │
│ │ 协调器) │◄────►│ 管理器) │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ │ ┌────────┴────────┐ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌──────────┐ │
│ └──►│ RM(资源 │ │ RM(资源 │ │
│ │ 管理器) │ │ 管理器) │ │
│ └──────────┘ └──────────┘ │
│ │
└──────────────────────────────────────────────────────┘
| 角色 | 英文 | 说明 |
|---|---|---|
| TC(事务协调器) | Transaction Coordinator | 独立部署的服务,负责维护全局事务和分支事务的状态,驱动全局提交或回滚 |
| TM(事务管理器) | Transaction Manager | 定义全局事务的范围:开始全局事务、提交或回滚全局事务 |
| RM(资源管理器) | Resource Manager | 管理分支事务处理的资源,与 TC 通信以注册分支事务和报告分支事务状态,并驱动分支事务提交或回滚 |
6.3 流程详解
两阶段流程
第一阶段:执行 + 注册
┌──────────────────────────────────────┐
│ TM:开启全局事务 │
│ → 向 TC 注册全局事务,获取 XID │
│ → 将 XID 传播到各个微服务(通过 RPC) │
└──────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ RM1 │ │ RM2 │ │ RM3 │
│ 执行SQL│ │ 执行SQL│ │ 执行SQL│
│ 保存前镜像│ │ 保存前镜像│ │ 保存前镜像│
│ 执行SQL│ │ 执行SQL│ │ 执行SQL│
│ 保存后镜像│ │ 保存后镜像│ │ 保存后镜像│
│ 生成undo│ │ 生成undo│ │ 生成undo│
│ 提交本地事务│ │ 提交本地事务│ │ 提交本地事务│
│ 注册分支事务│ │ 注册分支事务│ │ 注册分支事务│
│ 释放本地锁│ │ 释放本地锁│ │ 释放本地锁│
│ 获取全局锁│ │ 获取全局锁│ │ 获取全局锁│
└──────┘ └──────┘ └──────┘
│ │ │
└───────────────┼───────────────┘
▼
┌──────────────────────────────────────┐
│ TC:等待所有分支事务注册完成 │
│ → 收到所有注册后,向 TM 报告完成 │
└──────────────────────────────────────┘
第二阶段:全局提交
TC 根据所有分支事务的报告决定全局提交或回滚:
┌──── 全局提交 ────┐ ┌──── 全局回滚 ────┐
│ │ │ │
│ TC → RM: 异步删除 │ │ TC → RM: 异步回滚 │
│ undo 日志 │ │ 使用 undo 日志 │
│ │ │ 回复到前镜像状态 │
│ RM: 释放全局锁 │ │ RM: 释放全局锁 │
│ │ │ │
└───────────────────┘ └───────────────────┘
前镜像与后镜像
AT 模式的核心是自动生成 undo 日志,通过解析 SQL 语句,自动记录数据变更前后的快照:
// 执行 SQL 前,记录前镜像(Before Image)
SELECT id, name, balance FROM account WHERE id = 1;
// 前镜像: {id: 1, name: "张三", balance: 1000}
// 执行 UPDATE
UPDATE account SET balance = balance - 100 WHERE id = 1;
// 执行 SQL 后,记录后镜像(After Image)
SELECT id, name, balance FROM account WHERE id = 1;
// 后镜像: {id: 1, name: "张三", balance: 900}
// 生成的 undo 日志包含:
// 前镜像:{id: 1, name: "张三", balance: 1000}
// 后镜像:{id: 1, name: "张三", balance: 900}
// 回滚时,根据前镜像生成反向 SQL:
// UPDATE account SET balance = 1000 WHERE id = 1
6.4 全局锁与写隔离
AT 模式引入了全局锁的概念来解决并发写冲突:
┌──────────────────────────────────────────────────────┐
│ AT 模式的写隔离 │
├──────────────────────────────────────────────────────┤
│ │
│ 事务A(持有全局锁) │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 获取全局锁 ✓ │ │
│ │ 2. 执行 UPDATE,提交本地事务 │ │
│ │ 3. 注册分支事务 │ │
│ │ 4. 等待全局事务提交/回滚... │ │
│ │ 5. 全局事务完成后,释放全局锁 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 事务B(等待全局锁) │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 尝试获取全局锁 ✗(被事务A持有) │ │
│ │ 2. 等待全局锁释放... │ │
│ │ 3. 全局锁释放后,重新获取 │ │
│ │ 4. 继续执行 │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
写隔离机制: 在本地事务提交前,AT 模式会尝试获取该记录的全局锁。如果获取失败,则会等待或重试,直到超时。
6.5 补偿机制(重点)
AT 模式的补偿机制是其核心创新,相比于 2PC 和 TCC,它更加自动化:
自动回滚
AT 模式回滚流程:
1. TC 收到 TM 的全局回滚请求
2. TC 向所有已注册的 RM 发送分支回滚请求
3. RM 收到回滚请求后:
a. 通过 XID 和 Branch ID 找到对应的 undo 日志
b. 验证当前数据是否与后镜像一致(数据脏写校验)
c. 如果不一致,说明数据被其他事务修改,需要特殊处理
d. 如果一致,根据前镜像生成反向 SQL 执行回滚
e. 删除 undo 日志
f. 提交本地事务
4. 向 TC 报告回滚完成
数据脏写校验
数据校验逻辑:
回滚时,RM 会执行:
SELECT * FROM table WHERE id = ? FOR UPDATE -- 获取当前行数据
比较当前数据与 undo 日志中的后镜像:
情况 1:当前数据 == 后镜像
→ 数据未被其他事务修改,可以安全回滚
→ 执行反向 SQL:UPDATE ... SET balance = 1000 WHERE id = 1
情况 2:当前数据 != 后镜像
→ 数据被其他事务修改(脏写)
→ 无法自动回滚,需要人工处理
→ 策略:记录异常日志,发送告警,人工介入
补偿机制总结
┌──────────────────────────────────────────────────┐
│ AT 模式补偿机制总结 │
├──────────────────────────────────────────────────┤
│ │
│ 1. 自动生成 undo 日志 │
│ - 无需开发者编写补偿逻辑 │
│ - 通过解析 SQL 自动生成前镜像和后镜像 │
│ │
│ 2. 自动回滚 │
│ - 全局回滚时,TC 自动通知所有 RM 执行回滚 │
│ - RM 根据 undo 日志自动生成反向 SQL │
│ │
│ 3. 数据脏写校验 │
│ - 回滚前校验当前数据是否与后镜像一致 │
│ - 不一致时拒绝自动回滚,转人工处理 │
│ │
│ 4. 全局锁防止脏写 │
│ - 在本地事务提交前获取全局锁 │
│ - 全局事务完成前,其他事务无法修改同一行数据 │
│ │
│ 5. 补偿的幂等性 │
│ - undo 日志根据 XID + Branch ID 唯一标识 │
│ - 重复回滚请求不会产生副作用 │
│ │
│ 6. 异步补偿 │
│ - 第二阶段是异步的,不阻塞业务 │
│ - 提交/回滚失败会重试,直到成功或超时转为人工 │
│ │
└──────────────────────────────────────────────────┘
6.6 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 零侵入 | 对业务代码完全无侵入,只需添加一个注解 @GlobalTransactional,加上配置即可使用 |
| 自动补偿 | 无需开发者编写补偿逻辑,框架自动解析 SQL 生成 undo 日志和反向 SQL |
| 高性能 | 第一阶段提交本地事务后释放本地锁,只持有全局锁,锁粒度更细,并发性能更好 |
| 行业标准兼容 | 基于普通本地事务(JDBC 事务),不依赖 XA 协议,兼容所有主流数据库 |
| 易于集成 | 与 Spring Boot、Spring Cloud、Dubbo 等框架深度集成,开箱即用 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 仅支持关系型数据库 | 依赖数据库的 ACID 事务和 SQL 解析,不支持 NoSQL、消息队列等 |
| SQL 解析限制 | 仅支持 INSERT、UPDATE、DELETE 操作,不支持复杂 SQL(如存储过程、多表关联更新) |
| 全局锁开销 | 第二阶段提交前需要持有全局锁,高并发时可能成为瓶颈 |
| 数据一致性风险 | 第一阶段提交后到第二阶段提交前,数据对外可见(本地事务已提交),存在中间状态 |
| 脏写风险 | 虽然有全局锁,但在极端情况下(如全局锁超时),仍可能出现脏写 |
| 依赖 Seata Server | 需要独立部署 Seata Server(TC),增加了运维复杂度 |
| 数据膨胀 | undo_log 表会随着事务量增长而膨胀,需要定期清理 |
6.7 当前主流采用情况
AT 模式在国内互联网公司中非常流行,尤其是在阿里系生态中广泛使用:
- 阿里系:淘宝、天猫、菜鸟等核心业务系统大量使用 AT 模式
- 国内互联网公司:美团、滴滴、字节跳动等公司都有使用 Seata AT 模式
- 中小型企业:因为 AT 模式零侵入、易用的特性,在中小型企业中非常受欢迎
AT 模式是当前国内分布式事务的事实标准之一,但国际上更倾向于使用 Saga 模式。
七、可靠消息模式
7.1 概述
可靠消息模式(Reliable Message Pattern)是一种基于消息队列实现最终一致性的分布式事务方案。其核心思想是:通过消息中间件异步传递事务指令,上游服务先完成本地事务,再通过可靠的机制将消息投递给下游服务,保证上下游数据最终一致。
可靠消息模式不要求两阶段提交和回滚,而是通过"正向确认 + 重试"保证消息一定被消费,下游服务消费消息时通过幂等设计保证数据正确。
┌──────────┐ ① 发送半消息 ┌──────────┐
│ 上游服务 │ ───────────────► │ 消息中间件 │
│ (订单) │◄─────────────── │ (RocketMQ) │
└────┬─────┘ ② 半消息确认 └────┬─────┘
│ │
│ ③ 执行本地事务 │ ④ 提交/回滚消息
│ 订单入库 │
│ │
│ ▼
│ ┌──────────┐
└────────────────────────│ 下游服务 │
│ (库存) │
│ ⑤ 消费消息 │
│ ⑥ 扣减库存 │
└──────────┘
7.2 三种实现方案
可靠消息模式有三种主流实现方案:
方案一:本地消息表(Local Message Table)
原理: 在业务数据库中额外创建一张"本地消息表",将业务操作和消息记录放在同一个本地事务中,保证两者原子性。然后通过定时任务扫描消息表,将消息发送到消息队列。
┌────────────────────────────────────────────────────────────────┐
│ 本地消息表方案流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 上游服务: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 数据库事务(原子性保证) │ │
│ │ 1. 执行业务操作(如创建订单) │ │
│ │ 2. 插入本地消息表记录(状态=待发送) │ │
│ │ 3. 提交事务 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 定时任务(独立线程/进程): │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 扫描本地消息表,查询状态=待发送的记录 │ │
│ │ 2. 发送消息到 MQ │ │
│ │ 3. 发送成功 → 更新状态=已发送 │ │
│ │ 4. 发送失败 → 重试(指数退避) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 下游服务: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 消费 MQ 消息 │ │
│ │ 2. 幂等性检查(根据消息ID判断是否已处理) │ │
│ │ 3. 执行本地事务(如扣减库存) │ │
│ │ 4. 消费成功 → 确认消息 │ │
│ │ 5. 消费失败 → 重试或进入死信队列 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
本地消息表结构:
CREATE TABLE local_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_id VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
topic VARCHAR(128) NOT NULL COMMENT '消息主题',
tag VARCHAR(128) COMMENT '消息标签',
message_body TEXT NOT NULL COMMENT '消息体(JSON)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待发送, 1-发送成功, 2-发送失败',
retry_count INT DEFAULT 0 COMMENT '重试次数',
max_retry INT DEFAULT 10 COMMENT '最大重试次数',
next_retry_time DATETIME COMMENT '下次重试时间',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status_next_retry (status, next_retry_time)
);
代码示例:
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private LocalMessageDao messageDao;
// 创建订单 + 记录本地消息(同一个事务)
@Transactional
public void createOrder(OrderRequest request) {
// 1. 业务操作:创建订单
Order order = new Order();
order.setOrderId(generateOrderId());
order.setStatus("CREATED");
orderDao.insert(order);
// 2. 记录本地消息(与业务操作在同一事务中)
LocalMessage message = new LocalMessage();
message.setMessageId(generateMessageId());
message.setTopic("ORDER_CREATED");
message.setMessageBody(buildMessageBody(order));
message.setStatus(0); // 待发送
messageDao.insert(message);
}
}
// 定时任务:扫描并发送消息
@Component
public class MessageSendTask {
@Scheduled(fixedDelay = 5000) // 每 5 秒执行一次
public void sendMessages() {
// 查询待发送的消息
List<LocalMessage> messages = messageDao.findByStatus(0, 100);
for (LocalMessage msg : messages) {
try {
// 发送到 MQ
rocketMQTemplate.send(msg.getTopic(), msg.getMessageBody());
// 更新状态为发送成功
messageDao.updateStatus(msg.getMessageId(), 1);
} catch (Exception e) {
// 更新重试信息
messageDao.incrementRetry(msg.getMessageId(), calculateNextRetryTime(msg.getRetryCount()));
log.error("消息发送失败: {}", msg.getMessageId(), e);
}
}
}
}
// 下游服务:消费消息(幂等处理)
@Component
@RocketMQMessageListener(topic = "ORDER_CREATED", consumerGroup = "inventory_group")
public class InventoryConsumer implements RocketMQListener<String> {
@Autowired
private InventoryService inventoryService;
@Override
public void onMessage(String message) {
OrderEvent event = JSON.parseObject(message, OrderEvent.class);
String messageId = event.getMessageId();
// 幂等性检查:是否已处理过
if (inventoryService.isProcessed(messageId)) {
return; // 已处理,直接返回
}
// 执行业务操作
inventoryService.deductStock(event.getProductId(), event.getCount());
// 标记已处理
inventoryService.markProcessed(messageId);
}
}
方案二:事务消息(Transactional Message)
原理: 利用消息中间件提供的事务消息能力(如 RocketMQ 的事务消息、Kafka 的事务消息),将消息发送分为两个阶段:先发送半消息占位,等本地事务执行完后,再确认提交或回滚。
┌────────────────────────────────────────────────────────────────┐
│ RocketMQ 事务消息流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ ① 发送半消息(Half Message) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Producer │────────►│ RocketMQ │ │
│ │ │ │ (半消息不可消费)│ │
│ └──────────┘ └──────────┘ │
│ │
│ ② 执行本地事务 │
│ ┌──────────┐ │
│ │ Producer │ ──► 执行业务操作(如创建订单) │
│ │ │ ──► 成功或失败 │
│ └──────────┘ │
│ │
│ ③ 提交/回滚半消息 │
│ ┌──────────┐ 成功→COMMIT ┌──────────┐ │
│ │ Producer │ ──────────────►│ RocketMQ │ │
│ │ │◄──────────────│ │ │
│ └──────────┘ 失败→ROLLBACK │ 消息可消费│ │
│ └──────────┘ │
│ │
│ ④ 事务回查(如果步骤③因网络问题未到达 MQ) │
│ ┌──────────┐ 回查本地事务状态 ┌──────────┐ │
│ │ RocketMQ │ ────────────────►│ Producer │ │
│ │ │◄────────────────│ │ │
│ └──────────┘ 返回提交/回滚 └──────────┘ │
│ │
│ ⑤ 消费者消费消息 │
│ ┌──────────┐ ┌──────────┐ │
│ │ Consumer │◄────────│ RocketMQ │ │
│ │ │ │ (已提交消息)│ │
│ └──────────┘ └──────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
RocketMQ 事务消息代码示例:
// 生产者:发送事务消息
@Service
public class OrderTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrderCreatedMessage(Order order) {
// 构建消息
Message<String> message = MessageBuilder
.withPayload(JSON.toJSONString(order))
.build();
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order_tx_group", // 事务生产者组
"ORDER_CREATED", // Topic
message, // 消息
order // 附加参数,传给 executeLocalTransaction
);
if (result.getSendStatus() != SendStatus.SEND_OK) {
throw new RuntimeException("事务消息发送失败");
}
}
}
// 事务监听器:执行本地事务 + 本地事务回查
@RocketMQTransactionListener(txProducerGroup = "order_tx_group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderService orderService;
// 执行本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Order order = (Order) arg;
try {
// 执行本地事务:创建订单
orderService.createOrder(order);
// 本地事务成功 → 提交消息
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 本地事务失败 → 回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// 本地事务回查(MQ 长时间未收到 COMMIT/ROLLBACK 时触发)
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = JSON.parseObject((String) msg.getPayload(), Order.class).getOrderId();
// 查询本地事务执行结果
Order order = orderService.getOrder(orderId);
if (order != null && "CREATED".equals(order.getStatus())) {
// 本地事务已成功执行 → 提交消息
return RocketMQLocalTransactionState.COMMIT;
} else {
// 本地事务未执行或失败 → 回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
方案三:最大努力通知(Best Effort Notification)
原理: 上游服务完成本地事务后,通过消息队列或 HTTP 回调通知下游服务,下游服务处理失败时,上游服务会按一定策略(递增间隔、最大次数)多次重试通知,直到下游处理成功或达到最大重试次数。
┌────────────────────────────────────────────────────────────────┐
│ 最大努力通知流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 上游服务完成本地事务 │
│ │ │
│ ▼ │
│ 发送通知到下游服务 │
│ │ │
│ ├── 成功 → 结束 │
│ │ │
│ └── 失败 → 重试(间隔递增) │
│ │ │
│ ├── 第 1 次重试:1 分钟后 │
│ ├── 第 2 次重试:5 分钟后 │
│ ├── 第 3 次重试:10 分钟后 │
│ ├── 第 4 次重试:30 分钟后 │
│ ├── 第 5 次重试:1 小时后 │
│ │ │
│ └── 超过最大重试次数 → 人工介入 + 告警 │
│ │
└────────────────────────────────────────────────────────────────┘
典型应用场景: 支付回调通知、短信验证码发送、异步任务结果通知
// 最大努力通知实现
@Component
public class BestEffortNotificationService {
@Autowired
private RestTemplate restTemplate;
// 发送通知,失败后异步重试
public void notify(String callbackUrl, NotificationPayload payload) {
try {
restTemplate.postForEntity(callbackUrl, payload, String.class);
} catch (Exception e) {
// 异步重试
retryTask.asyncRetry(callbackUrl, payload, 0);
}
}
}
@Component
public class RetryTask {
private static final int[] RETRY_INTERVALS = {60, 300, 600, 1800, 3600}; // 秒
private static final int MAX_RETRY = 5;
@Async
public void asyncRetry(String callbackUrl, NotificationPayload payload, int retryCount) {
if (retryCount >= MAX_RETRY) {
// 超过最大重试次数,记录异常,人工处理
alertService.sendAlert("通知失败,需人工处理", callbackUrl, payload);
return;
}
// 等待指定间隔后重试
Thread.sleep(RETRY_INTERVALS[retryCount] * 1000);
try {
restTemplate.postForEntity(callbackUrl, payload, String.class);
// 成功,结束
} catch (Exception e) {
// 继续重试
asyncRetry(callbackUrl, payload, retryCount + 1);
}
}
}
7.3 三种方案对比
| 维度 | 本地消息表 | 事务消息(RocketMQ) | 最大努力通知 |
|---|---|---|---|
| 实现复杂度 | 中 | 低(MQ 原生支持) | 低 |
| 可靠性 | 高 | 高 | 中 |
| 实时性 | 中(依赖定时任务间隔) | 高(准实时) | 低(依赖重试间隔) |
| 消息中间件依赖 | 低(任意 MQ) | 高(需支持事务消息的 MQ) | 低(任意 MQ 或 HTTP) |
| 额外存储 | 需要(消息表) | 不需要 | 需要(重试记录表) |
| 适用场景 | 通用场景 | 需要高实时性的场景 | 允许一定延迟的异步通知 |
7.4 补偿机制(重点)
可靠消息模式的补偿机制与前面几种方案有本质区别:它不依赖回滚,而是依赖"正向重试"保证最终一致性。
核心补偿策略
┌──────────────────────────────────────────────────────────────┐
│ 可靠消息模式补偿机制 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 消息发送失败补偿 │
│ - 本地消息表:定时任务扫描未发送消息,持续重试 │
│ - 事务消息:MQ 通过事务回查确保消息被提交或回滚 │
│ - 最大努力通知:按递增间隔重试,直到成功或达到上限 │
│ │
│ 2. 消息消费失败补偿 │
│ - MQ 消费重试:consumer 返回失败时,MQ 自动重试 │
│ - 死信队列:重试超过上限后进入死信队列,人工处理 │
│ - 幂等性保证:消费端通过幂等性设计,保证重复消费不会 │
│ 产生副作用 │
│ │
│ 3. 上游已成功、下游未成功的补偿 │
│ - 这是可靠消息模式的核心补偿场景 │
│ - 上游事务已提交,但下游消费失败或未消费到消息 │
│ - 补偿方式:通过重试投递 + 死信队列 + 人工对账 │
│ │
│ 4. 人工对账补偿(终极兜底) │
│ - 定时对账:通过定时任务对比上下游数据差异 │
│ - 数据修复:发现差异后自动或人工修复数据 │
│ - 告警通知:异常数据通过告警通知运维人员 │
│ │
└──────────────────────────────────────────────────────────────┘
幂等性设计(关键)
可靠消息模式中,幂等性设计是补偿机制的核心。因为消息可能被重复投递,消费端必须保证幂等。
幂等性实现方案:
1. 唯一约束法(数据库方案)
┌──────────────────────────────────────────────────┐
│ CREATE TABLE inventory_deduction_log ( │
│ id BIGINT AUTO_INCREMENT PRIMARY KEY, │
│ message_id VARCHAR(64) NOT NULL UNIQUE, ← 唯一约束 │
│ product_id VARCHAR(64), │
│ count INT, │
│ create_time DATETIME │
│ ); │
│ │
│ 消费逻辑: │
│ 1. INSERT INTO inventory_deduction_log (message_id, ...) │
│ 2. 如果 INSERT 成功(无重复),执行业务操作 │
│ 3. 如果 INSERT 失败(唯一约束冲突),说明已处理,跳过│
└──────────────────────────────────────────────────┘
2. Redis 去重法
┌──────────────────────────────────────────────────┐
│ String key = "msg:deduction:" + messageId; │
│ Boolean exists = redis.setIfAbsent(key, "1", │
│ 24, TimeUnit.HOURS); │
│ if (!exists) { │
│ return; // 已处理过,跳过 │
│ } │
│ // 执行业务操作 │
└──────────────────────────────────────────────────┘
3. 版本号法(乐观锁)
┌──────────────────────────────────────────────────┐
│ UPDATE inventory │
│ SET stock = stock - #{count}, │
│ version = version + 1 │
│ WHERE product_id = #{productId} │
│ AND version = #{expectedVersion} │
│ │
│ 如果 affected_rows == 0,说明版本已变,已处理过 │
└──────────────────────────────────────────────────┘
死信队列与补偿
消息消费失败 → 自动重试(1~16次,指数退避)
├── 重试成功 → 消费确认
└── 重试全部失败 → 进入死信队列(Dead Letter Queue)
│
├── 监控告警:通知运维人员
│
├── 人工排查:分析失败原因
│ ├── 业务逻辑错误 → 修复代码 → 重新消费
│ ├── 数据依赖问题 → 修复数据 → 重新消费
│ └── 临时故障 → 手动重试即可
│
└── 数据对账:定时任务对比上下游数据,修复差异
7.5 优缺点
优点:
| 优点 | 说明 |
|---|---|
| 高可用 | 上游和下游通过消息队列异步解耦,两者互不阻塞,可用性极高 |
| 高性能 | 异步处理,不锁定资源,吞吐量极高,适合高并发场景 |
| 松耦合 | 上下游服务仅通过消息交互,不直接依赖,易于独立扩展和部署 |
| 实现简单 | 不需要实现 Try/Confirm/Cancel 或补偿逻辑,只需保证消息可靠投递和幂等消费 |
| 流量削峰 | 消息队列天然支持流量削峰填谷,应对突发流量 |
缺点:
| 缺点 | 详细说明 |
|---|---|
| 实时性差 | 异步处理,数据存在延迟,不适合需要实时一致性的场景 |
| 数据不一致窗口 | 消息投递到消费处理之间存在时间窗口,期间数据可能不一致 |
| 下游失败处理复杂 | 下游消费失败后,只能重试或人工处理,无法"回滚"上游已提交的事务 |
| 依赖消息中间件 | 需要额外部署和运维消息队列,增加了系统复杂度 |
| 消息丢失风险 | 极端情况下(如 MQ 故障、磁盘损坏),消息可能丢失,需要额外保障 |
| 幂等性要求高 | 所有消费端必须实现幂等,否则重复消息会导致数据错误 |
| 排查困难 | 异步链路长,涉及多个组件,出问题时排查链路复杂 |
7.6 当前主流采用情况
可靠消息模式是互联网公司中应用最广泛的分布式事务方案之一,原因如下:
- 技术门槛低:不需要实现复杂的分布式事务协议,只需消息队列 + 幂等性设计
- 与异步架构天然契合:微服务广泛使用消息队列进行异步通信,可靠消息模式是自然延伸
- 性能优异:异步解耦,不阻塞,支持高并发、大流量
代表实践:
- 几乎所有互联网公司都在使用:订单创建后异步通知各下游服务
- RocketMQ 事务消息:阿里巴巴、字节跳动等广泛使用
- Kafka 事务消息:国际公司偏好使用 Kafka 实现类似能力
- 本地消息表:老系统改造或无法使用事务消息的场景
典型应用场景:
- 订单创建 → 通知库存服务扣减库存
- 用户注册 → 发送欢迎邮件/短信
- 支付成功 → 通知订单系统更新状态
- 数据变更 → 通知搜索引擎更新索引
八、阿里 Seata 分布式事务框架
8.1 概述
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,前身是阿里内部的 TXC(Taobao Transaction Constructor)和 GTS(Global Transaction Service),于 2019 年 1 月正式开源。
Seata 提供了一套高性能、零侵入、一站式的分布式事务解决方案,支持 AT、TCC、Saga 和 XA 四种事务模式。
8.2 核心架构
┌──────────────────────────────────────────────────────────────┐
│ Seata 整体架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Seata Server(TC) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 事务协调 │ │ 锁管理 │ │ 会话管理 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ 存储模式:File / DB / Redis │ │
│ │ 集群部署:支持高可用 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌────┴────────┐ ┌──────┴──────────┐ │
│ │ 服务A(TM+RM)│ │ 服务B(RM) │ │
│ │ │ │ │ │
│ │ @GlobalTransactio│ │ 执行SQL │ │
│ │ nal │ │ 注册分支事务 │ │
│ │ 调用服务B │────────►│ 报告状态 │ │
│ │ │ RPC │ │ │
│ └──────────────┘ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
8.3 四种事务模式详解
模式一:AT 模式(默认)
原理: 基于本地事务 + 自动生成 undo 日志的两阶段提交
使用方式:
// 只需加一个注解,零侵入
@GlobalTransactional(timeoutMills = 300000, name = "create-order")
public void createOrder(OrderRequest request) {
// 业务逻辑:操作多个数据库
orderService.save(order); // 订单服务
inventoryService.deduct(sku); // 库存服务
accountService.deduct(money); // 账户服务
}
适用场景: 数据库操作、强一致性需求、希望零侵入的业务
模式二:TCC 模式
原理: 用户自定义 Try/Confirm/Cancel 接口
使用方式:
// 定义 TCC 接口
@LocalTCC
public interface AccountTccService {
@TwoPhaseBusinessAction(
name = "deductAccount",
commitMethod = "commit",
rollbackMethod = "rollback"
)
boolean prepare(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
boolean commit(BusinessActionContext context);
boolean rollback(BusinessActionContext context);
}
适用场景: 对一致性要求极高的场景(如金融、支付)、需要精确控制资源预留的场景
模式三:Saga 模式
原理: 编排器式 Saga,长事务拆分为多个本地事务,失败时逆序补偿
使用方式: 通过状态机 DSL(JSON)定义 Saga 流程
{
"Name": "orderSaga",
"Type": "Saga",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService.createOrder",
"CompensateState": "CancelOrder",
"Next": "DeductStock"
},
"CancelOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService.cancelOrder",
"End": true
},
"DeductStock": {
"Type": "ServiceTask",
"ServiceName": "inventoryService.deductStock",
"CompensateState": "RestoreStock",
"Next": "DeductBalance"
},
"RestoreStock": {
"Type": "ServiceTask",
"ServiceName": "inventoryService.restoreStock",
"Next": "CancelOrder"
},
"DeductBalance": {
"Type": "ServiceTask",
"ServiceName": "paymentService.deductBalance",
"CompensateState": "RefundBalance",
"End": true
},
"RefundBalance": {
"Type": "ServiceTask",
"ServiceName": "paymentService.refundBalance",
"Next": "RestoreStock"
}
}
}
适用场景: 长事务流程(如订单流程、审批流程)、需要异步处理、老系统改造(无需改造接口)
模式四:XA 模式
原理: 基于 XA 协议的 2PC,使用数据库的 XA 事务支持
使用方式:
# 配置文件中指定模式为 XA
seata.data-source-proxy-mode=XA
@GlobalTransactional
public void createOrder() {
// 业务逻辑
// Seata 自动使用 XA 协议协调数据库事务
}
适用场景: 对一致性要求极高(CP 系统)、需要跨数据库厂商的标准化方案、传统企业应用迁移
8.4 四种模式对比
| 维度 | AT 模式 | TCC 模式 | Saga 模式 | XA 模式 |
|---|---|---|---|---|
| 一致性 | 最终一致 | 最终一致 | 最终一致 | 强一致 |
| 隔离性 | 读已提交 | 业务层隔离 | 无隔离 | 全局锁隔离 |
| 侵入性 | 零侵入 | 高侵入 | 中侵入 | 零侵入 |
| 性能 | 高 | 高 | 高 | 低 |
| 补偿方式 | 自动(undo 日志) | 手动(Cancel 接口) | 手动(补偿接口) | 自动(数据库) |
| 数据库要求 | 关系型数据库 | 无限制 | 无限制 | 支持 XA 的数据库 |
| 开发成本 | 低 | 高 | 中 | 低 |
| 运维成本 | 中(需 Seata Server) | 中(需 Seata Server) | 中(需 Seata Server) | 中(需 Seata Server) |
| 适用场景 | 通用业务 | 金融/支付 | 长事务/老系统 | 强一致性场景 |
8.5 Seata 的补偿机制(重点)
Seata 为不同模式提供了不同的补偿机制:
AT 模式补偿
AT 模式补偿 = 自动 undo 日志回滚
1. 执行 SQL 时自动生成 undo 日志(前镜像 + 后镜像)
2. 全局回滚时,TC 通知各 RM 执行回滚
3. RM 根据 undo 日志生成反向 SQL
4. 回滚前校验数据一致性(后镜像匹配)
5. 回滚后删除 undo 日志
补偿特点:
- 自动补偿,无需开发者编码
- 有数据脏写校验
- 补偿失败时转为人工处理
TCC 模式补偿
TCC 模式补偿 = 用户自定义 Cancel 方法
1. 协调者调用各服务的 Cancel 接口
2. 开发者需要处理:
- 空回滚(Try 未执行时收到 Cancel)
- 防悬挂(Cancel 先于 Try 到达)
- 幂等性(Cancel 可能被重试)
3. Cancel 失败后重试,超时后人工处理
补偿特点:
- 业务层补偿,精确可控
- 需要处理空回滚、防悬挂、幂等
- 开发成本高但灵活性好
Saga 模式补偿
Saga 模式补偿 = 逆序补偿 + 正向重试
1. 事务失败时,按逆序执行补偿
2. 补偿失败后重试,可配置重试策略
3. 支持正向重试(非致命错误时重试当前步骤)
4. 支持状态机驱动的自动补偿
补偿特点:
- 逆序执行补偿,保证事务完整性
- 支持状态机驱动的自动补偿
- 需要为每个正向操作编写补偿逻辑
XA 模式补偿
XA 模式补偿 = 数据库 XA 协议回滚
1. 与 2PC 相同,使用数据库的 XA 事务
2. 回滚由数据库自动完成
3. 无需 Seata 额外处理补偿逻辑
补偿特点:
- 数据库自动回滚,最可靠
- 但性能最差,锁定时间长
8.6 Seata 的使用步骤
# 1. 引入依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
# 2. 配置 Seata
seata:
tx-service-group: my_tx_group
registry:
type: nacos
nacos:
server-addr: localhost:8848
config:
type: nacos
nacos:
server-addr: localhost:8848
# 3. 配置 undo_log 表(AT 模式必须)
CREATE TABLE undo_log (
id BIGINT NOT NULL AUTO_INCREMENT,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
);
# 4. 业务代码中使用
@GlobalTransactional
public void createOrder(OrderRequest request) {
// 正常业务逻辑
}
8.7 Seata 的优缺点
优点:
| 优点 | 说明 |
|---|---|
| 多模式支持 | 同时支持 AT、TCC、Saga、XA 四种模式,覆盖不同业务场景 |
| 零侵入(AT/XA) | AT 和 XA 模式对业务代码零侵入,只需一个注解 |
| 高性能 | AT 模式在第一阶段提交本地事务后释放本地锁,性能优于 2PC |
| 生态完善 | 与 Spring Cloud、Dubbo、Nacos 等阿里生态深度集成 |
| 社区活跃 | 开源项目,社区活跃,版本迭代快,文档完善 |
| 企业级验证 | 在阿里内部经历了双十一等大流量考验 |
缺点:
| 缺点 | 说明 |
|---|---|
| 需要独立部署 Seata Server | 增加了运维成本和复杂度,Seata Server 本身也是单点(需集群部署) |
| SQL 解析限制 | AT 模式仅支持 INSERT、UPDATE、DELETE,不支持复杂 SQL |
| 仅支持关系型数据库 | AT 模式依赖数据库事务,不支持 NoSQL |
| 全局锁开销 | 高并发下全局锁可能成为瓶颈 |
| 学习成本 | 配置项较多,概念较多,需要时间学习和调试 |
| 版本兼容性 | 版本升级时可能有 breaking changes,需要关注兼容性 |
8.8 当前主流采用情况
Seata 是目前国内最主流的分布式事务框架,原因如下:
- 阿里背书:经过阿里内部大规模验证,可靠性有保障
- 多模式覆盖:AT 模式零侵入适合快速接入,TCC 模式适合高要求场景,Saga 模式适合长事务
- 生态完善:与 Spring Cloud Alibaba、Dubbo、Nacos 等无缝集成
- 社区活跃:GitHub 上 25k+ Star,持续更新维护
国际对比:
| 特性 | Seata | Atomikos | Narayana | Bitronix |
|---|---|---|---|---|
| 模式 | AT/TCC/Saga/XA | XA | XA/Saga | XA |
| 零侵入 | ✅(AT/XA) | ❌ | ❌ | ❌ |
| 社区 | 活跃 | 一般 | 一般 | 已停止维护 |
| 国内使用 | 广泛 | 少 | 极少 | 极少 |
九、总结对比
9.1 各方案对比总表
| 维度 | 2PC | 3PC | TCC | Saga | AT 模式 | 可靠消息 |
|---|---|---|---|---|---|---|
| 一致性 | 强一致 | 强一致 | 最终一致 | 最终一致 | 最终一致 | 最终一致 |
| 隔离性 | 全局锁 | 全局锁 | 业务层 | 无 | 全局锁+本地锁 | 无 |
| 性能 | 低 | 低 | 高 | 高 | 高 | 极高 |
| 可用性 | 低 | 中 | 高 | 高 | 高 | 极高 |
| 侵入性 | 低 | 低 | 高 | 中 | 零 | 低 |
| 实现复杂度 | 低 | 中 | 高 | 中 | 低(框架实现) | 中 |
| 补偿方式 | 自动 | 自动+超时 | 手动Cancel | 手动补偿 | 自动undo | 正向重试+对账 |
| 资源锁定 | 长时间 | 长时间 | 无 | 无 | 短时间(全局锁) | 无 |
| 实时性 | 高 | 高 | 高 | 中 | 高 | 中 |
| 长事务支持 | 差 | 差 | 中 | 好 | 中 | 好 |
| 当前主流度 | 低 | 极低 | 高 | 高 | 高(国内) | 极高 |
9.2 选型建议
┌──────────────────────────────────────────────────────────────────┐
│ 分布式事务选型决策树 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 需要强一致性? │
│ ├── 是 → 2PC/XA 或 3PC │
│ │ ├── 可接受性能差 → 2PC/XA │
│ │ └── 需要减少阻塞 → 3PC │
│ │ │
│ └── 否 → 最终一致性即可 │
│ ├── 需要实时一致性? │
│ │ ├── 是 → 同步方案 │
│ │ │ ├── 希望零侵入? │
│ │ │ │ ├── 是 → AT 模式(Seata) │
│ │ │ │ └── 否 → 继续判断 │
│ │ │ ├── 金融/支付场景? │
│ │ │ │ ├── 是 → TCC 模式 │
│ │ │ │ └── 否 → 继续判断 │
│ │ │ └── 长事务/工作流/老系统改造? │
│ │ │ ├── 是 → Saga 模式 │
│ │ │ └── 否 → AT 模式(默认推荐) │
│ │ │ │
│ │ └── 否 → 可接受异步 + 延迟 │
│ │ ├── 已有 MQ 基础设施 → 事务消息(RocketMQ) │
│ │ ├── 无 MQ 或简单场景 → 本地消息表 │
│ │ └── 回调通知场景 → 最大努力通知 │
│ │ │
│ └── 混合场景 → Seata 多模式 或 可靠消息 + TCC 组合 │
│ │
└──────────────────────────────────────────────────────────────────┘
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 通用互联网业务 | Seata AT 模式 | 零侵入、高性能、易用 |
| 异步解耦场景 | 可靠消息模式 | 高性能、松耦合、天然适合微服务 |
| 金融/支付核心链路 | TCC 模式 | 业务层精确控制,补偿可靠 |
| 长事务/审批流程 | Saga 模式 | 无锁、适合长时间运行 |
| 传统企业应用 | 2PC/XA | 强一致性、数据库原生支持 |
| 老系统改造 | 可靠消息(本地消息表)或 Saga | 无需改造现有接口 |
| 高并发削峰 | 可靠消息模式 | 异步处理,消息队列天然削峰 |
| 混合场景 | Seata 多模式 | 根据业务需要选择不同模式 |
9.3 最终建议
- 大多数互联网业务场景:优先选择 Seata AT 模式,零侵入、高性能,能满足 90% 的业务需求
- 异步解耦/高并发削峰:优先选择 可靠消息模式,利用消息队列实现高性能异步处理
- 资金安全要求高的场景:选择 TCC 模式,虽然开发成本高,但对资金的精确控制是必要的
- 长事务/工作流流程:选择 Saga 模式,无锁设计适合长时间运行的事务
- 避免使用 2PC/3PC:除非是传统企业应用或对强一致性有硬性要求,否则不建议使用
- 关键原则:能用 AT 就用 AT,需要异步解耦用可靠消息,不行再考虑 TCC 或 Saga,尽量避免 2PC
文档版本: v1.1
更新日期: 2026 年 7 月
更多推荐



所有评论(0)