单体里的一个
@Transactional就能搞定的事,到了微服务就变成头号难题——跨库、跨服务、跨进程,本地事务管不到。本篇按「强一致 → 最终一致」的脉络,梳理五种主流分布式事务方案。
一、问题背景与理论基础
1.1 为什么会有分布式事务
原始业务场景:用户下单 → 锁票 → 创建订单 → 扣费 → 交票。在单体里,这是一个 @Transactional 包住的本地事务。一旦拆成微服务:
- Ticket、Order、User 三个服务,三个独立的数据库
- 三个服务在两个系统中,根本无法使用统一的事务管理
- 任一步失败,前面已成功的步骤无法回滚
1.2 CAP 与 BASE
CAP 与 BASE 是分布式事务的理论基石,本节只做指针——完整推导见 CAP 与 BASE:分布式系统的理论基石。
| 理论 | 含义 | 实践取舍 |
|---|---|---|
| CAP | 一致性 / 可用性 / 分区容错 三选二 | 分布式系统 P 必选,剩 C vs A |
| BASE | Basically Available + Soft state + Eventually consistent | 牺牲强一致,换取可用性 + 最终一致 |
分布式事务的所有方案,本质都是在 C 和 A 之间做取舍:
- 强一致(CP):XA / 2PC——锁资源、效率低,但数据严格一致
- 最终一致(AP):消息驱动 / TCC / 本地消息表 / Saga——可用性好,但有短暂不一致窗口
二、方案一:消息驱动(可靠消息最终一致性)
2.1 整体流程

核心思想:把一条业务流程拆成多个消息驱动的步骤,每个服务消费消息后处理本地事务,并向 MQ 投递下一状态消息。
新订单 ──► [Ticket 锁票] ──► 订单创建 ──► [Order 创建订单] ──► 待缴费
│
◄── [Order 完成订单] ◄── 交票 ◄── [Ticket 交票] ◄── [User 扣费] ◄┘
- 每一步操作都通过消息传递
- 模块内部可以使用 JTA 这样的强一致性或弱一致性、最终一致性等机制
- 全套流程能实现最终的一致性
2.2 强一致性的丧失
整条业务线无法实现强一致性。如走到第三步:
- User 服务扣费了
- 但没到第四步(交票)
- 用户此时查询,能看到自己钱被扣除了,却没票
→ 这就是「短暂不一致窗口」,需要业务设计上容忍(如「出票中」状态、超时查询、客服兜底)。
2.3 对 MQ 的要求
- 要求 MQ 支持 JMS 事务:DB 处理完,提交 MQ 事务,再提交 DB 事务;如果 MQ 事务提交而 DB 事务提交失败,则回滚 MQ 事务
- 消息可能重复发送:所以处理消息要做幂等(消息重新发送的情况:消息投递时未收到投递确认消息可重发)
2.4 失败处理策略
对执行过程中小模块失败的情况,要做处理:
- 将失败信息记录到 MQ 的错误队列,做回滚
- 使用定时任务扫描超时任务处理
- 保存信息人工处理(兜底)
2.5 简单实现的代码与缺陷
每个模块内服务的调用,最朴素的写法:
public void trans() {
try {
// 1. 操作数据库
boolean result = dao.update(data); // 操作数据库失败,会抛出异常
// 2. 如果数据库操作成功则发送消息
if (result) {
mq.send(data); // 如果方法执行失败,会抛出异常
}
} catch (Exception e) {
rollback(); // 如果发生异常,就回滚
}
}

这个写法存在严重缺陷:
- DB 处理成功,MQ 发送消息但未收到消息确认(第 4 步异常)
- 服务认为 MQ 发送失败并回滚 DB
- 实际上消息已经发到下游——下游重复消费,DB 已回滚,数据不一致
核心矛盾:DB 和 MQ 是两个独立资源,不能用本地事务同时管。这就是后面「本地消息表」要解决的问题。
2.6 异步事件通知——外部事件服务
改进方案:使用异步事件通知——引入外部事件服务。

时序图关键步骤:
| 步骤 | 动作 |
|---|---|
| 1-2 | 微服务 A 开启事务、更新 DB |
| 3 | 向消息代理投递事件(消息代理先持久化事件,不立即发送) |
| 4 | 事件投递成功回执 |
| 5 | 消息代理异步投递给微服务 B |
| 6 | 微服务 A 提交本地事务 |
| 7 | 微服务 B 处理完成回执 |
关键设计:DB 处理业务提交前,向 MQ 发送事件,MQ 只记录事件,并不发送;当 DB 提交或回滚后,再通知 MQ 发送或删除事件。
为防止业务通知事件「发送或删除」时故障,另外起一套定时任务监听 MQ 队列中的消息,将消息去 DB 业务查询该事件是否需要发送或删除。
缺点:
- 业务与事件的通信由原来的 1 次交互变为 2 次(一次发送事件,一次提交/回滚事件),多了网络开销
- 业务需要给事件服务提供额外的接口校验该事件是否已经处理完成
三、方案二:XA 协议的两阶段提交(2PC)
3.1 协议规范
XA 协议是 X/Open 组织提出的分布式事务规范:
- 由一个事务管理器(TM)和多个资源管理器(RM)组成
- 提交分为两个阶段:prepare 和 commit
3.2 第一阶段:prepare

- TM 向所有 RM 发送
prepare指令 - 各 RM 执行事务操作,把 undo/redo 写入日志,但不提交
- 各 RM 返回
ready(就绪)或abort(失败)
3.3 第二阶段:commit / rollback

- 所有 RM 都 ready → TM 发送
commit,各 RM 提交并返回committed - 任一 RM abort 或超时 → TM 发送
rollback,各 RM 回滚
3.4 特点
| 维度 | 评价 |
|---|---|
| 一致性 | ✅ 保证数据库的强一致性 |
| 可用性 | ❌ 任一 RM 故障,整个事务阻塞 |
| 性能 | ❌ 性能与本地事务比相差 10 倍(资源锁定时间长) |
| 异常 | commit 阶段部分 RM 出现问题,事务不一致,需要手动介入人工处理 |
3.5 使用
- MySQL 5.7 以上版本均支持 XA 协议
- MySQL Connector/J 5.0+ 支持 XA
- Java 系统中,数据源较为流行的实现是 Atomikos(充当事务管理器 TM)
现代视角(2026)补一句:XA 在 2019 年已经是「能不用就不用」的方案——性能太差、协调者单点问题、commit 阶段异常难以自动恢复。今天 XA 几乎只在金融核心系统出现,互联网业务一律走最终一致。Seata 的 AT 模式虽然在概念上借鉴了 2PC,但用本地事务 + undo_log 实现了无锁的「软 2PC」,是 XA 的现代替代。
四、方案三:TCC(Try-Confirm-Cancel)
4.1 核心思想
每个需要实现事务的业务需要三个接口:
| 接口 | 作用 |
|---|---|
tryXXX | 业务检查,预留资源(如冻结额度、占库存) |
confirmXXX | 执行业务,使用资源 |
cancelXXX | 回滚业务,释放资源 |
三个接口由协调器统一管理调用。TCC 借鉴 XA 的统一资源管理,但是不是两阶段提交。
关键差异:不同资源之间没有锁,事务过程中数据没有锁,没有隔离。TCC 把「隔离性」交给业务自己实现(通过
try阶段的资源冻结)。
4.2 幂等性约束
由于分布式系统的架构设计,无法保证调用各个业务的
commit/cancel方法的有序性与次数,所以commit/cancel方法需要满足幂等性。
这是 TCC 落地最容易被忽略、也最容易踩坑的一点——网络重试、协调器重启都会导致 confirm/cancel 被调用多次。
4.3 协调器职责
协调器接管事务管理,类似于 JTA 的独立事务管理器:
- 保存每个资源上的事务记录:跟踪状态,检查超时
- 保证每个资源的事务性
- 处理各种错误:超时、重试、网络异常、服务不可用
4.4 实现一:REST 接口方式
@RestController
public class UserResource {
@Transactional
public void tryCharge(DTO dto) { /* TODO */ }
@Transactional
public void confirmCharge(DTO dto) { /* TODO */ }
@Transactional
public void cancelCharge(DTO dto) { /* TODO */ }
}

每个服务暴露三个 REST 端点,协调器通过 HTTP 调用。
4.5 时序示例

| 步骤 | 动作 |
|---|---|
| 1 | 下单(外部触发 Order 服务) |
| 2-3 | Order 开启事务,向协调器注册新事务 |
| 4 | Order 调用 User 的 tryCharge()(Try 阶段) |
| 5 | User 返回资源路径,Order 向协调器注册分支事务 |
| 6 | Order 处理本地订单业务 |
| 8 | Order 通知协调器事务完成 |
| 9 | 协调器异步调用 User 的 confirm/cancel |
注意步骤 9 是异步——主流程(步骤 1-8)已经返回给用户了,confirm/cancel 由协调器后台触发。这是 TCC 性能优于 XA 的关键。
4.6 实现二:Service 规范
public interface TccMethod<P extends TccRequest<R>, R extends Serializable> {
R doTry(P param); // Try 阶段执行业务逻辑
void doConfirm(P param); // Confirm 阶段确认提交
void doCancel(P param); // Cancel 阶段回滚
}

通过泛型约束:
P extends TccRequest<R>——参数必须是 TCC 请求类型R extends Serializable——返回值可序列化(跨进程调用需要)
业务实现这个接口后,由 TCC 框架(如 Hmily、ByteTCC、EasyTransaction)接管事务协调。
五、方案四:本地消息表
5.1 思路
采用 BASE 原理保证事务的最终一致性。
这是工程上最常用的方案——把「分布式事务」问题降维成「本地事务 + 定时任务」问题。
核心做法:
- 业务库中新增一张「本地消息表」,与业务表在同一个数据库
- 业务操作和写消息表放在一个本地事务——要么都成功,要么都失败
- 一个定时任务扫描消息表,把未发送的消息投递给 MQ
- 投递成功后,更新消息表状态为「已发送」
- 下游服务消费消息,处理完成后回 ACK
┌──── 本地数据库(一个事务) ────┐
│ 1. update business_table │
│ 2. insert local_message_table│
└────────────────────────────────┘
│
▼ (定时任务扫描)
投递到 MQ ──► 下游消费
│
▼ (ACK)
更新 message 状态 = SENT
5.2 优势
| 优势 | 说明 |
|---|---|
| 无需引入协调器 | 业务库 + MQ + 定时任务即可,依赖少 |
| 本地事务保证 | 业务和消息表在同一事务,强一致 |
| 下游解耦 | 通过 MQ 解耦,下游失败可重试 |
| 工程落地简单 | 比起 TCC,业务侵入性低 |
5.3 缺点
- 耦合业务库:消息表与业务库耦合,业务库性能受影响
- 定时任务存在延迟:扫描频率 = 最终一致性窗口(典型 1-10s)
- 重复消费:定时任务可能重复投递,下游必须幂等
现代视角(2026)补一句:本地消息表思想在 2024 年后演化成了 「事务消息」(如 RocketMQ 的事务消息、阿里 Seata 的 Saga/AT 模式)——把「消息表」内置到 MQ 服务端,业务感知不到。但本质还是同一套思路。
六、方案五:Saga 模式(长事务)
这套原文笔记没写,但 2017 年以后 Saga 在微服务圈已经成了长事务的事实标准——补一段。
6.1 思路
Saga 把一个长事务拆成 N 个本地事务 T1, T2, ..., Tn,每个本地事务都有对应的补偿事务 C1, C2, ..., Cn:
- 正常执行:
T1 → T2 → T3 → ... → Tn - 中途失败:执行已完成步骤的反向补偿,如 T3 失败则
C2 → C1
正向:T1 ─► T2 ─► T3 ─► T4 ✅ 完成
│
✗ 失败
▼
补偿: C2 ◄─ C1 🔄 回滚
6.2 两种实现
| 模式 | 流程 | 适用 |
|---|---|---|
| 编排式(Choreography) | 各服务监听事件,没有中心协调器 | 服务数少,链路简单 |
| 协调式(Orchestration) | 有一个 Saga 协调器统一调度 | 服务数多,复杂流程 |
6.3 vs TCC
| 维度 | TCC | Saga |
|---|---|---|
| 阶段 | Try / Confirm / Cancel | Tn(业务) / Cn(补偿) |
| 隔离性 | Try 阶段预留资源 | 无隔离——中间状态可见 |
| 业务侵入 | 高(要写 3 个接口) | 中(多写一个补偿接口) |
| 适用 | 短事务、需要隔离 | 长事务、多步骤 |
选择经验:业务流程 ≤ 3 步、且对中间状态可见性敏感 → TCC;业务流程长、能容忍中间状态 → Saga。
七、工业实现:Seata 三种模式(2026 视角补充)
阿里 2019 年开源的 Seata(Simple Extensible Autonomous Transaction Architecture)是国内分布式事务的事实标准框架。它把上述思想工程化为三种内置模式:
| 模式 | 对应方案 | 特点 |
|---|---|---|
| AT 模式 | 无锁 2PC(变种) | 默认推荐——业务零侵入,自动生成 undo_log 反向 SQL |
| TCC 模式 | TCC | 业务自定义 try/confirm/cancel |
| Saga 模式 | Saga | 状态机驱动,适合长流程 |
| XA 2PC | 1.2+ 引入,需 DB 支持 XA |
AT 模式的精妙:业务只写普通的
@Transactional,Seata 拦截 SQL,自动生成 undo_log。提交前所有分支事务都是本地事务,全局提交时异步清理 undo_log,全局回滚时用 undo_log 反向恢复。代价:AT 模式靠全局锁保证写隔离,性能不如 TCC,但业务零侵入是它最大的杀手锏——这也是为什么 2026 年 Seata AT 模式仍是国内中小厂首选。
八、五种方案对比总结
| 方案 | 一致性 | 性能 | 复杂度 | 业务侵入 | 适用场景 |
|---|---|---|---|---|---|
| XA / 2PC | 强一致 | ❌ 差 | 中 | 低 | 金融核心、传统行业 |
| TCC | 最终一致(隔离好) | ✅ 好 | 高 | 高(3 接口) | 高并发短事务,资金类 |
| 消息驱动 | 最终一致 | ✅ 好 | 中 | 中 | 业务流程清晰的链路 |
| 本地消息表 | 最终一致 | ✅ 好 | 低 | 低 | 工程首选、中小业务 |
| Saga | 最终一致(无隔离) | ✅ 好 | 中 | 中(补偿接口) | 长事务、多步骤 |
8.1 选型口诀
强一致选 XA,高并发选 TCC,工程首选本地消息表,长事务选 Saga,已有 Seata 直接上 AT。
8.2 通用要求
无论哪种方案,这三条是共识:
| 要求 | 说明 |
|---|---|
| 业务幂等 | MQ 重试、协调器重试都会发生,confirm/cancel/消费都必须幂等 |
| 可观测 | 分布式事务调试极难,必须能查每一步状态 |
| 兜底预案 | 任何方案都有覆盖不到的异常,必须有人工补偿通道 |
九、与之前笔记的串联
本篇只讲事务机制,业务侧的拆分逻辑见 DDD 系列——限界上下文(Bounded Context) 怎么定,决定了你有多少个微服务、有多少跨服务事务需要做。可以对照看:
事务的本地基础见 MySQL 隔离级别与 Spring 事务——理解不了本地事务的隔离级别,分布式事务的取舍就是空中楼阁。
消息的幂等性讨论见 Kafka(下):消费者、消息有序性、命令与集群——消息驱动方案落地必读。
十、小结
分布式事务没有银弹。所有的方案,本质都是在「一致性 / 可用性 / 性能 / 业务侵入」四个维度上做权衡。
| 时代 | 主流方案 |
|---|---|
| 2010 年前 | XA / 2PC(强一致,金融行业) |
| 2010-2018 | TCC / 本地消息表(互联网大厂) |
| 2019-2024 | Seata AT/TCC/Saga(一站式框架) |
| 2025+ | Service Mesh + Dapr(把事务能力下沉到基础设施层) |
核心知识到 2026 年没变——CAP/BASE、幂等、补偿、两阶段,这套底层原理再放 10 年依然适用。变的只是「谁来管事务」:业务代码 → 中间件 → 基础设施。
现代视角补一句(2026):今天的趋势是让业务忘记分布式事务的存在——Seata AT 已经做到了「写
@GlobalTransactional就完事」,Service Mesh 把事务能力下沉到 Sidecar。业务代码层越来越干净,事务复杂度全部下沉到平台——这是分布式事务演化的终极方向。