分布式系统中的分布式事务
一次和 AI 的对话整理。从基础定义开始,逐步深入解释分布式事务各个发展阶段下主要解决什么问题以及同时带来什么新的问题。记录下来主要是为了把容易混淆的概念彻底搞清楚。
分布式系统中的分布式事务
一、基础概念
什么是分布式事务
本地事务:单个数据库内,一组 SQL 要么全成功、要么全失败(ACID)。
分布式事务:一组操作分散在多个独立的服务/数据库/系统上,业务上要求"要么全成功,要么全失败"。
典型场景(电商下单):
- 订单服务(DB1):创建订单
- 库存服务(DB2):扣减库存
- 账户服务(DB3):扣减余额
三个动作在不同节点上,但业务上必须是一个整体。
核心问题
跨多个独立系统时的数据一致性——避免"一半成功一半失败"的脏局面。
一个关键认知
单机数据库免费送给你的 ACID,在分布式下都要自己造一遍。
所有方案的本质差异,无非是:
- 协调逻辑放在哪一层(数据库层 / 中间件层 / 应用层)
- 用什么手段造(锁 / 冻结字段 / 补偿操作 / 重试)
- 牺牲哪些 ACID 属性来换性能(隔离性几乎都被牺牲)
理论基础:CAP 与 BASE
CAP 定理(2000 年 Eric Brewer 提出):一致性(C)、可用性(A)、分区容错性(P),三者只能选二。互联网系统必须要 P,所以在 C 和 A 之间取舍。
BASE 理论(2008 年 eBay 提出):Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致)。
核心哲学转变:从"不一致就是 bug"转向"短暂不一致可以接受,只要最终一致"。
二、2PC(两阶段提交)
历史背景
1978 年 Jim Gray 提出,1980 年代被标准化为 XA 协议。是分布式事务的"祖师爷"。
解决什么问题
让多个互不相识的独立数据库协作完成一个事务,保证强一致性。
核心思想
引入一个协调者(Coordinator)统一指挥所有参与者(Participant):先让大家"准备好",再统一决定提交或回滚。
执行流程
阶段一:准备(Prepare)
- 协调者向所有参与者发
Prepare - 参与者执行 SQL、写 undo/redo 日志、锁住资源,但不提交
- 回复 Yes(可以提交)或 No
阶段二:提交(Commit)
- 所有人都 Yes → 协调者发
Commit,参与者真正提交、释放锁 - 有人 No 或超时 → 协调者发
Rollback,参与者回滚、释放锁
时序图
协调者 参与者A 参与者B
│ │ │
│── Prepare ───────>│ │
│── Prepare ────────────────────────────>│
│ │ 执行+锁资源 │
│ │ 执行+锁资源 │
│<── Yes ───────────│ │
│<── Yes ─────────────────────────────────│
│ (决策:全员同意) │ │
│── Commit ────────>│ │
│── Commit ──────────────────────────────>│
│ │ 真正提交、解锁 │
│ │ 真正提交、解锁 │
│<── Ack ───────────│ │
│<── Ack ──────────────────────────────────│
思考:分布式事务是不是控制一组本地事务的提交或回滚?
方向正确,但有个关键认知需要注意:已提交的本地事务无法回滚。所以 2PC 的协调必须发生在"执行完但未提交"这个中间状态——参与者在 Prepare 阶段执行 SQL 但卡在"准备好提交"的特殊状态(MySQL 里叫 XA PREPARE),等协调者的最终指令。这个"延迟提交"是 2PC 区别于后续方案(TCC/Saga 是立即提交 + 补偿)的本质。
致命问题
1. 同步阻塞:参与者从 Prepare 开始就锁着资源等指令,性能差。
2. 协调者单点故障:
- 阶段二中途宕机最致命
- 参与者卡在"已准备"状态(in-doubt transaction),既不敢提交也不敢回滚
- 只能死锁资源等协调者恢复
3. 数据不一致:阶段二发 Commit 只发了一半就宕机 + 网络问题 → 部分提交、部分未提交,无法自动修复。
4. 锁的辐射效应:木桶效应——最慢的参与者拖累所有人。锁的持续时间 = max(本地事务) + 2 轮网络 + 协调者耗时。
思考:2PC 的两个主要问题是不是资源锁和协调者可用性?
方向正确,但要更精确:
- 锁的时长不只是"最长的本地事务",还包含两轮网络往返 + 协调者决策。木桶效应——一个参与者慢,所有人陪着锁资源等。
- 协调者故障要分时机:阶段一前/中崩溃,参与者超时回滚就行,不算太糟;阶段二中途崩溃最致命,参与者陷入 in-doubt 状态,资源永久阻塞。
- 还有第三个问题——数据不一致:阶段二指令部分送达 + 协调者宕机时,会出现真正的数据不一致。这正是 2PC 即使叫"强一致"也不能 100% 保证一致的原因。
适用场景
- 传统企业系统、金融核心系统
- 对一致性要求极高、规模不大、并发不高
- 互联网公司基本不用(性能扛不住)
实际应用
MySQL XA、PostgreSQL prepared transactions、JTA(Java Transaction API)
思考:能不能让数据库通过分布式事务 ID 直接控制一组本地事务?
这个直觉是对的,业界已经存在两种实现:
- 单分布式数据库内部:TiDB、Spanner、OceanBase 等,业务只看到普通 SQL,数据库内部自己跑 2PC——你想要的"让数据库自己搞定"已经是现实。
- 跨多个独立数据库:XA 协议就是这种思路的标准化版本,应用层通过全局事务 ID 串起各数据库的本地事务,但本质还是 2PC。
但有边界:跨服务、跨外部 API 的分布式事务,数据库管不到——必须应用层介入。所以"协调可以下沉、可以隐藏,但只要数据分散在多个独立单元,协调本身就消除不掉"。
三、3PC(三阶段提交)
解决什么问题
针对 2PC 的两大痛点改进:
- 协调者单点故障导致参与者无限阻塞
- 阶段二中途宕机导致数据不一致
核心思想
两个关键改动:
- 把"提交阶段"拆成两步(CanCommit → PreCommit → DoCommit)
- 给参与者加超时自决机制——协调者失联时,参与者能自己做决定
三个阶段
| 阶段 | 名称 | 做什么 |
|---|---|---|
| 阶段一 | CanCommit | 只询问能不能干,不锁资源 |
| 阶段二 | PreCommit | 真正执行 SQL、写日志、锁资源 |
| 阶段三 | DoCommit | 真正提交、释放锁 |
超时自决(核心创新)
| 参与者所处状态 | 协调者失联时 | 怎么做 |
|---|---|---|
| 阶段一后 | 超时 | 直接 Abort(没锁资源,没损失) |
| 阶段二后 | 超时 | 自己 Commit(赌协调者已决定提交) |
逻辑:进入 PreCommit 状态说明所有参与者都已回复 Yes——事务大概率会成功。
解决了 vs 没解决
✅ 解决:参与者不再无限阻塞、协调者宕机影响降低
❌ 没解决:
- 多一轮网络往返,性能更差
- 网络分区下依然可能不一致(超时自动 Commit 是个赌注,协调者本意可能是 Abort)
- 复杂度更高
工业界为什么不用 3PC
处于尴尬的中间地带:比 2PC 复杂、比 Paxos 弱、比 Saga 慢。理论价值大于实际价值。
要强一致 → Paxos/Raft;要高性能 → 最终一致性方案。
四、TCC(Try-Confirm-Cancel)
解决什么问题
2PC 锁数据库时间太长的性能瓶颈。让本地事务立即提交,把"锁"从数据库层下放到业务层。
核心思想
把每个业务操作拆成三个动作:
| 阶段 | 动作 | 做什么 |
|---|---|---|
| Try | 预留资源 | 检查 + 冻结资源(不真扣) |
| Confirm | 确认执行 | 真正扣减冻结的资源 |
| Cancel | 撤销预留 | 释放冻结的资源 |
关键:每个动作都是独立的本地事务,立即提交、立即释放数据库锁。
示例:转账(A 转 100 给 B)
Try 阶段:
- A:
balance -= 100, frozen_balance += 100(钱"挂起",未真扣) - B:
pre_received += 100(B 还看不到这 100)
Confirm 阶段(Try 全部成功):
- A:
frozen_balance -= 100(钱真扣) - B:
pre_received -= 100, balance += 100(B 真能用)
Cancel 阶段(Try 任一失败):
- A:
frozen_balance -= 100, balance += 100(退回) - B:
pre_received -= 100(清掉)
执行流程
TCC 协调者
│
① Try(并行调用所有服务)
│
全部成功?
├─ Yes → ② Confirm(所有服务真正生效)
└─ No → ③ Cancel(已 Try 的服务释放冻结)
解决了 2PC 的什么问题
✅ 锁时间极短:每个 Try 立即提交,DB 锁瞬间释放 ✅ 高并发友好:业务字段级"冻结",粒度比 DB 行锁更可控 ✅ 协调者宕机不阻塞:参与者已提交,没人在等锁 ✅ 可跨异构系统:MySQL + Redis + 外部 API 都能纳入
TCC 的新问题
1. 业务侵入极强(最大痛点)
思考:TCC 是不是每个操作都要定义三个方法?
完全正确。每个参与分布式事务的操作都要写 Try/Confirm/Cancel 三个方法。一个原本 100 行的业务方法,TCC 化后可能要 1000+ 行(含幂等、空回滚、防悬挂、测试)。加上数据库表要加冻结字段,业务模型本身要重新设计。框架(Seata/Hmily)只能帮忙做调度,三个方法的业务逻辑必须自己写。
- 每个操作要写 3 个方法(开发量翻 5-10 倍)
- 数据库表要加字段(冻结余额、预收金额等)
- 业务模型要重新设计
2. 必须保证三大特性
| 特性 | 问题描述 | 后果 |
|---|---|---|
| 幂等性 | 网络抖动导致重复调用 | 多次执行结果必须一致 |
| 空回滚 | Try 还没执行就收到 Cancel | Cancel 必须能识别"没东西要撤销" |
| 防悬挂 | Try 因延迟比 Cancel 晚到 | Cancel 后到的 Try 不能再执行 |
3. 隔离性问题
思考:Try 阶段冻结的数据被读到怎么办?事务回滚后读到的是错误数据吗?
这是 TCC 真实存在的痛点——全局事务的隔离性被牺牲了。Try 提交后到 Cancel 之间存在时间窗口(正常毫秒到秒级,异常下可能更长),外界能读到中间状态。如果应用基于这个状态做了决策,事务回滚后这个决策就是基于错误数据。
工程上的应对是让业务代码"知道"冻结的存在,分情况读取(查可用余额走 balance,查总资产走 balance + frozen);并接受"非核心读路径可能看到短暂的不一致数据"这一现实。TCC 的本质是"分布式补偿",保证最终的数据正确性,不保证过程中任意时刻的数据正确性。
4. TCC 的锁还在,只是搬家了:从数据库层下放到业务字段层(如 frozen_balance)。锁的对象更精细、释放更快。
TCC vs 2PC
| 维度 | 2PC | TCC |
|---|---|---|
| 锁的位置 | 数据库行锁 | 业务字段(应用级) |
| 锁的时长 | 整个事务期间 | 每个 Try 提交后立即释放 |
| 协调对象 | 数据库 | 业务服务 |
| 失败处理 | 数据库回滚 | 业务补偿(Cancel) |
| 业务侵入 | 几乎无 | 极强 |
适用场景
✅ 适合:金融、支付、电商核心交易(资金/库存类,需严格保证不丢不重) ❌ 不适合:业务简单(改造成本高)、长流程业务(10+ 步骤用 Saga 更合适)
主流框架
Seata(TCC 模式)、Hmily、ByteTCC、DTM
五、Saga
历史背景
1987 年 Hector Garcia-Molina 提出,原本用于解决长事务问题。被冷落 20 多年后,因微服务架构兴起被重新挖出。
解决什么问题
长流程业务的一致性——TCC 改造 3 个方法成本太高,且不是所有业务都有"预留"语义。
核心思想
TCC 是"先预留再确认",Saga 是"先干再补偿"。
把大事务拆成一串小事务(每个立即提交),失败时按反向顺序执行补偿操作抵消。
正向:T1 → T2 → T3 → ... → Tn
失败:Cn → C(n-1) → ... → C1(反向补偿)
示例:下单流程
正向:
T1: 创建订单
T2: 扣减库存
T3: 扣减余额
T4: 通知物流
T3 失败时:
C2: 恢复库存
C1: 取消订单
(T4 未执行,无需补偿)
关键区别于 TCC:Saga 是真扣了钱,补偿是真退钱;TCC 只是冻结,Cancel 是解冻。
两种执行模式
编排式(Orchestration):中央协调器按顺序调用各服务。流程清晰、易监控,生产首选。主流框架:Seata(Saga 模式)、Temporal、AWS Step Functions。
编舞式(Choreography):无中央协调器,各服务通过事件互相驱动。松耦合但调试难,容易事件地狱,慎用。
Saga 的核心难题
1. 补偿事务必须自己开发
思考:Saga 是不是也要为每个正向操作开发补偿逻辑?
是的。每个正向操作都要写配对的补偿方法,而且补偿不是简单写个反向 SQL——要处理幂等、空补偿、防悬挂、补偿失败、不可补偿操作等一堆边界问题。框架能帮你管"什么时候调用补偿",但"补偿具体怎么写"是业务代码自己的事。
虽然 Saga 比 TCC 少一个方法(不用 Try),但补偿的业务复杂度往往超出预期——TCC 的 Cancel 只要解冻就行,Saga 的补偿要"真撤销"已经发生的变更。
2. 业务必须能"补偿"
| 操作 | 能补偿吗 |
|---|---|
| 扣库存 → 加库存 | ✅ |
| 扣款 → 退款 | ✅ |
| 发短信 | ❌(发出去收不回) |
| 打印发票 | ❌(物理动作不可撤销) |
| 调用第三方支付 | ⚠️ 看对方是否支持退款 |
应对:不可补偿操作放在 Saga 最后一步,或接受"业务级补偿"(再发一条"对不起"短信)。
3. 隔离性完全丧失(比 TCC 更严重)
- TCC:外界看到"冻结",未真变更
- Saga:外界看到真实的中间变更
Saga 隔离性的工程应对
思考:Saga 的隔离性问题有解吗?
没有银弹,只有缓解和规避。这是 Garcia-Molina 在 1987 年原始论文里就坦承的局限。
| 方案 | 思路 | 效果 |
|---|---|---|
| 语义锁 | 加 PROCESSING 状态字段,标记 saga_id | 最常用,业务侧约定 |
| 可交换更新 | 改记流水,而不是直接改余额 | 数学上优雅,但要重新设计业务 |
| 业务流程隔离 | Saga 期间冻结所有相关读写 | 影响用户体验 |
| 版本化数据 | 追加写入,只读 COMMITTED 版本 | 存储成本高 |
| 补偿后通知 | 接受脏读,事后通知下游 | 解决不了"基于错误数据的决策" |
| 合理设计顺序 | 敏感操作放后面、不可补偿操作放最后 | 应作为基本原则 |
生产经典组合:语义锁 + 合理顺序 + 补偿后通知 + 监控告警
Saga vs TCC
| 维度 | TCC | Saga |
|---|---|---|
| 方法数 | 3 个(Try/Confirm/Cancel) | 2 个(正向/补偿) |
| 资源处理 | 预留(冻结) | 直接执行(真改) |
| 中间状态 | 冻结状态,外界看不到真变化 | 真实变更,完全暴露 |
| 隔离性 | 较好 | 最差 |
| 业务侵入 | 大 | 中(少一个方法、通常不改表) |
| 业务复杂度 | 简单(解冻就行) | 复杂(要真撤销) |
| 适合场景 | 资金类、强一致 | 长流程、容忍中间不一致 |
一个重要洞察
Saga 不只是"事务方案",本质上是一种工作流引擎。近几年 Temporal、Camunda 等工作流引擎在海外火起来——它们天然支持"长流程 + 补偿 + 重试 + 监控",把 Saga 当工作流的一种模式来用,更自然。
六、本地消息表
解决什么问题
TCC 和 Saga 业务侵入太大、改造成本高。本地消息表用异步化的思路绕开了同步协调,是更轻量的最终一致性方案。
工作方式
把"消息"也存到自己的数据库里,和业务数据一起提交本地事务——保证"业务变更 + 消息记录"原子。然后异步任务轮询消息表,把消息发到 MQ,下游消费完成最终一致。
本地事务:
UPDATE account SET balance = balance - 100
INSERT INTO local_message (status = PENDING)
COMMIT ← 业务变更 + 消息记录 原子提交
异步任务:
轮询 PENDING 消息 → 发送到 MQ → 更新为 SENT
解决的问题
- 没有协调者,没有锁
- 业务侵入小(只多一个消息表)
- 消息持久化在自己库里,绝不丢
- 实现简单,普通 RabbitMQ/Kafka 都行
存在的问题
- 需要额外轮询任务
- 延迟较大(几秒到几十秒)
- 消息表会膨胀,需定期清理
- 不保证消息顺序
- 只能做"单向"操作——消息事务保证消息送达,不保证下游一定成功
适用场景
中小团队、对延迟不敏感、业务量不大——朴素的"够用就行"方案。
七、MQ 事务消息
解决什么问题
本地消息表需要轮询,延迟高。RocketMQ 事务消息把消息可靠性做进 MQ 本身,省去轮询。
工作方式
引入半消息(Half Message)机制:
1. 应用 → MQ:发送"半消息"(已存入 MQ,但消费者不可见)
2. 应用 → 执行本地事务
3. 应用 → MQ:Commit(半消息变正常)或 Rollback(删除)
关键设计:回查机制——如果第 3 步丢了,MQ 主动询问应用"事务到底成没成",保证最终状态收敛。
解决的问题(相比本地消息表)
- 延迟低(毫秒级,无轮询)
- 不用业务方维护消息表
- MQ 主动回查,可靠性更强
存在的问题
- 强依赖 RocketMQ(Kafka 等不支持)
- 应用必须实现回查接口
- 消费方依然要处理幂等(MQ 至少一次投递)
- 消息顺序问题依然存在
消息事务整体特点
优点:不锁数据、无中心协调者、业务侵入小、性能好 缺点:只适合"下游一定能成功"的单向操作;异步带来的延迟用户能感知
适用场景
✅ 互联网业务主流:高并发、能容忍短暂不一致 ❌ 不适合:金融核心交易、必须同步看到结果的场景
八、方案演进与全景对比
演进逻辑
1970-80s 2PC/XA 强一致,性能不重要
↓
1980s 3PC 改进 2PC,工业界没火起来
↓
2000s CAP/BASE 理论支撑,开始放弃强一致
↓
2007 TCC 业务层补偿,资金类核心场景
↓
2010s Saga 复活 长流程、微服务架构
↓
2010s 消息事务 异步化,互联网主流
↓
现代 分布式数据库 Spanner/TiDB 把复杂度藏到 DB 内部
核心驱动力:规模和性能。系统从"机房几台机器"变成"全球几万台机器",2PC 那种"等所有人确认"的模式就崩了。
三维抽象
思考:分布式事务的本质是不是事务管理器协调一组本地事务?
方向对,但要扩展:
- "事务管理器"不是必需的——Saga 编舞式、消息事务都没有中央协调者,靠事件或消息驱动。
- "回滚 vs 补偿"二分法不够全面——更准确的分类是延迟提交+回滚(2PC)、预留+确认(TCC)、执行+补偿(Saga)、异步最终一致(消息事务)。
更本质的表达:分布式事务的本质是用"额外的元信息 + 额外的协调逻辑",模拟出本来由数据库免费提供的 ACID 保证。 单机数据库免费送的东西,分布式下都要自己造一遍。
所有方案的核心差异可归结为三个维度的取舍:
| 维度 | 选项 |
|---|---|
| 何时提交本地事务 | 延迟提交 / 立即提交 |
| 如何处理失败 | 回滚 / 补偿 / 重试 |
| 协调机制 | 中央协调者 / 事件驱动 / 消息队列 |
| 方案 | 何时提交 | 失败处理 | 协调机制 |
|---|---|---|---|
| 2PC | 延迟 | 真回滚 | 中央协调者 |
| TCC | 立即(冻结态) | 补偿(解冻/确认) | 中央协调者 |
| Saga 编排式 | 立即(真实态) | 反向补偿 | 中央协调者 |
| Saga 编舞式 | 立即(真实态) | 反向补偿 | 事件驱动 |
| 消息事务 | 立即 + 消息 | 重试到成功 | 消息队列 |
| 本地消息表 | 立即 + 消息表 | 重试到成功 | 消息表 + 轮询 |
全景对比
| 方案 | 一致性 | 性能 | 开发成本 | 隔离性 | 典型场景 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致 | 差 | 低 | 好 | 传统企业、金融核心 |
| TCC | 较强一致 | 中 | 高 | 较好 | 资金、库存等核心交易 |
| Saga | 最终一致 | 较好 | 中高 | 差 | 长流程、跨多服务 |
| 消息事务 | 最终一致 | 好 | 较低 | 回避 | 互联网业务主流 |
| 强一致 DB | 强一致 | 中 | 极低 | 好 | Spanner/TiDB |
九、Seata 框架(重点)
定位
阿里 2019 年开源的一站式分布式事务框架,国内事实标准。把 2PC/TCC/Saga/XA 统一封装。
核心架构:三个角色
┌──────────┐
│ TC │ ← 协调者(独立部署)
│ 协调者 │
└─────┬────┘
│
┌─────────┴─────────┐
│ │
┌────┴────┐ ┌────┴────┐
│ TM │ │ RM │
│ 事务管理 │ │ 资源管理 │
└─────────┘ └─────────┘
(业务发起方) (每个业务服务)
| 角色 | 全称 | 部署位置 | 职责 |
|---|---|---|---|
| TC | Transaction Coordinator | 独立部署的 Seata Server | 总管:分配 XID、记录状态、决策提交/回滚 |
| TM | Transaction Manager | 业务服务(发起方) | 定义全局事务边界 |
| RM | Resource Manager | 每个业务服务 | 管理本服务的本地资源 |
类比:TM = 召集人;TC = 领队;RM = 每个参与者。
Seata 支持的四种模式
| 模式 | 对应方案 | 业务侵入 | 适用场景 |
|---|---|---|---|
| AT | 改进版 2PC(自动补偿) | 极小 | 90% 业务(默认) |
| TCC | 标准 TCC | 大 | 高并发资金类 |
| Saga | 状态机式 Saga | 中 | 长流程业务 |
| XA | 标准 XA | 极小 | 必须强一致 |
AT 模式(Seata 的招牌)
业务代码长这样
@GlobalTransactional // 就这一个注解
public void createOrder() {
orderService.create();
stockService.deduct();
accountService.deduct();
}
业务方不写 Try/Confirm/Cancel,不写补偿逻辑——这是 AT 模式的核心卖点。
核心机制:SQL 拦截 + 前后镜像
阶段一:业务 SQL 执行时
业务 SQL:UPDATE account SET balance = balance - 100 WHERE id = 1
Seata 实际执行:
1. 解析 SQL(用 Druid Parser),提取表名、主键、WHERE 条件
2. SELECT 前镜像:{ id: 1, balance: 500 }
3. 执行业务 SQL
4. SELECT 后镜像:{ id: 1, balance: 400 }
5. 前后镜像写入 undo_log 表
6. 本地事务提交 ← 注意:直接提交了!
关键:业务 SQL 和 undo_log 写入在同一个本地事务,原子提交。
阶段二:全局事务决议
- 所有分支成功 → TC 通知各 RM 异步删除 undo_log
- 任何分支失败 → 根据 undo_log 反向生成回滚 SQL 执行回滚
AT 本质上是什么
Seata 官方说是"改进版 2PC",本质上更像自动化的 Saga:
| 维度 | 标准 2PC | Seata AT |
|---|---|---|
| 阶段一 | Prepare(锁资源不提交) | 直接提交(记 undo_log) |
| 阶段二 | Commit/Rollback | 删 undo_log / 反向 SQL 补偿 |
| DB 锁时间 | 整个事务期间 | 只在本地事务期间 |
| 回滚机制 | 数据库 rollback | 反向 SQL 补偿 |
AT = Saga 的思路 + 自动生成补偿 SQL + 全局锁机制
全局锁(保证隔离性)
分支事务执行时:
1. 解析 SQL,找出修改的行(主键)
2. 向 TC 申请该行的"全局锁"
3. 拿到锁才能继续
4. 本地事务提交后,全局锁继续持有
5. 全局事务结束(提交或回滚)才释放
效果:同一行数据,同一时刻只有一个全局事务能修改——避免脏写。
限制:
- 只防写写冲突,不防读
- 绕过 Seata 的直接修改无法防护
回滚时的脏写检查
1. 查询当前数据
2. 对比"当前数据"和"后镜像"
- 一致 → 数据没被改过,安全回滚
- 不一致 → 脏写!无法自动回滚,告警人工处理
3. 根据前镜像生成回滚 SQL 执行
4. 删除 undo_log
AT 的限制
思考:Seata 怎么准确生成补偿 SQL?复杂 SQL 解析失败怎么办?
Seata 不靠"理解 SQL 语义",而是靠数据本身的快照(前后镜像)。但这套机制有大量限制:
- 必须是关系型数据库(MySQL、Oracle、PostgreSQL)
- 表必须有主键(用主键定位行做镜像)
- 某些 SQL 不支持:复杂多表 JOIN、子查询、函数表达式、DDL
- 性能损耗:每个 SQL 多 2 次 SELECT(前后镜像)
- 批量操作性能急剧下降
当 Druid Parser 无法解析时,Seata 的策略是快速失败 + 全局回滚——直接抛 SQLException,业务方法异常返回,全局事务回滚,其他分支用 undo_log 回滚。
最危险的情况:Parser 觉得解析对了但实际不对 → 生成错误的镜像 → 用错误镜像生成回滚 SQL → 数据错乱。所以实际工程中必须配合 SQL 规范约束 + 监控告警 + 对账兜底。
工程应对策略
1. 制定严格的 SQL 规范
- 涉及全局事务的表必须有主键
- UPDATE/DELETE 用主键作 WHERE
- 禁止多表 JOIN 更新
- 禁止数据库函数(NOW()、UUID() 等)
2. SQL 拆分(把复杂 SQL 拆成简单 SQL)
3. 多模式混合
- 简单 CRUD → AT
- 资金核心 → TCC
- 长流程 → Saga
- 复杂分析 → 不放进分布式事务
4. 配套监控 + 对账系统兜底
Seata TCC 模式
开发者自己实现 Try/Confirm/Cancel 三个方法,Seata 负责调度:
@LocalTCC
public interface AccountService {
@TwoPhaseBusinessAction(name = "deductAction",
commitMethod = "confirm",
rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext ctx, ...);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
Seata 提供框架支持(事务生命周期、幂等/空回滚/防悬挂的辅助),但业务逻辑要自己写。
Seata Saga 模式
用 JSON 状态机 DSL 定义流程和补偿。好处:可视化、易维护;坏处:要学 DSL。
Seata 工作流程(以 AT 为例)
1. TM 开启全局事务
TM → TC:开启事务,TC 生成 XID
2. TM 调用各服务(XID 透传)
订单服务 → 库存服务 → 账户服务(都带 XID)
3. 每个服务的 RM 执行本地事务
- 拦截 SQL、生成前后镜像
- 写 undo_log
- 注册分支事务(用 XID 关联)
- 申请全局锁
- 提交本地事务
4. 全部成功 → TM 提交全局事务
TM → TC:提交
TC → 各 RM:异步删除 undo_log + 释放全局锁
5. 任何失败 → TM 回滚
TM → TC:回滚
TC → 各 RM:反向回滚 + 释放全局锁
Seata 部署
[业务服务A] [业务服务B] [业务服务C]
│ │ │
└───────────┴───────────┘
│
┌──────────────┐
│ Seata Server │ ← 独立部署
│ 集群(TC) │ 3 节点高可用
└───────┬──────┘
│
┌──────────────┐
│ MySQL/Redis │ ← 存事务状态
└──────────────┘
依赖:注册中心(Nacos/Eureka)、配置中心(Nacos/Apollo)、持久化存储(MySQL/Redis)
Seata 优势
✅ 多模式一站式(AT/TCC/Saga/XA) ✅ AT 模式零侵入(最大卖点) ✅ 生态完善(Spring Cloud / Dubbo) ✅ 国内成熟(阿里背书、中文文档) ✅ 可视化控制台
Seata 实际工程中的坑
- AT 模式"假高可用":复杂 SQL 解析失败、表必须有主键、全局锁死锁、undo_log 膨胀
- Seata Server 是性能瓶颈:高并发下 TC 是瓶颈、挂了所有事务卡住
- 调试和排查困难:链路复杂、日志分散,需配套 APM(SkyWalking)
- 版本兼容性:升级常有 breaking change
- 不是银弹:救不了不合理的业务设计、性能瓶颈
一个重要认知
Seata 降低了分布式事务的工程门槛,但不消除分布式事务的复杂度。
很多团队的演进路径:
项目初期:纯 AT 模式(图方便)
↓ 各种坑
核心场景迁移到 TCC
↓
次要场景迁移到消息事务
↓
最终:AT + TCC + 消息事务 + 对账系统的混合方案
十、工程实践建议
选型决策树
你的业务特征是什么?
│
├─ 需要强一致(金融核心、证券撮合)
│ ├─ 能用分布式 DB(TiDB/Spanner)→ 用分布式数据库
│ └─ 不能 → 2PC/XA 或 Seata XA
│
├─ 涉及钱、库存等"硬资产"
│ └─ TCC(Seata TCC 或 Hmily)
│
├─ 长流程业务(5+ 步骤跨多服务)
│ └─ Saga 或工作流引擎(Temporal)
│
├─ 普通微服务、能容忍最终一致
│ ├─ 简单 → Seata AT
│ └─ 大量异步 → 消息事务(RocketMQ/本地消息表)
│
└─ 容忍延迟、对性能敏感
└─ 本地消息表 + 异步处理
混合方案才是常态
核心交易链路(扣款、扣库存、订单状态)
→ TCC 或 Seata AT
长流程业务(订单全生命周期)
→ Saga 或工作流引擎
次要异步操作(发短信、加积分、写日志)
→ 消息事务
最终防线
→ 对账系统(定期扫描数据一致性)
必备的工程实践
- 对账系统:任何分布式事务方案都不是 100% 可靠的,对账是最后兜底
- 幂等性设计:所有接口默认按"会被重复调用"设计
- 监控告警:长时间未完成的事务、补偿失败、消费失败、undo_log 异常
- 人工补偿入口:自动化失败时,能手动介入处理
- APM 工具:分布式链路追踪(SkyWalking、Pinpoint)
反思:要不要用分布式事务
最好的分布式事务,是没有分布式事务。
很多分布式事务问题源于业务划分不合理——服务粒度过细、跨服务的事务边界过大、数据库设计不合理。
优先考虑:
- 重新划分服务边界,把强关联的操作放进同一个服务
- 用最终一致 + 对账,而不是追求强一致
- 用分布式数据库(TiDB 等)把事务问题留在 DB 层
避免:"用 Seata 解决所有问题"——它只是工具,不是架构。
十一、特殊场景:应用分布式 + 数据库统一
思考:如果不考虑跨数据库和跨服务的事务,多个服务最终操作同一个数据库系统,用自带分布式事务的分布式数据库是不是更好?
完全正确。这是个被忽视但越来越常见的场景。
场景定义
传统分布式事务场景:
订单服务 → 订单 DB(MySQL)
库存服务 → 库存 DB(MySQL)
账户服务 → 账户 DB(PostgreSQL)
↑ 多个独立的数据库实例
本场景:
订单服务 ──┐
库存服务 ──┼──→ 同一个分布式数据库(如 TiDB 集群)
账户服务 ──┘
↑ 应用是分布式的,数据库是统一的
关键认知:数据库统一了,分布式事务的复杂度就回到了"单机事务"模型——业务只需要写普通 SQL,DB 内部自己解决跨节点协调。
这种场景下分布式数据库的优势
1. 业务代码极简
// 用 Seata AT
@GlobalTransactional
public void createOrder() {
orderService.create();
stockService.deduct();
accountService.deduct();
}
// 用 TiDB(同一个数据库连接)
@Transactional
public void createOrder() {
orderService.create();
stockService.deduct();
accountService.deduct();
}
差别:
- Seata:要部署 TC、配置注册中心、加 undo_log 表、踩各种坑
- TiDB:就一个普通
@Transactional注解,和单机 MySQL 几乎无差别
2. 真正的强一致性
分布式数据库真正实现了 ACID(包括隔离性),不像 TCC/Saga 是"伪一致性":
| 维度 | Seata AT / TCC / Saga | 分布式数据库 |
|---|---|---|
| 原子性 | 通过补偿模拟 | 真原子 |
| 一致性 | 最终一致 | 强一致 |
| 隔离性 | 几乎丧失 | 完整隔离级别(RC、RR、Serializable) |
| 持久性 | 业务库各自保证 | 多副本保证 |
前面讨论的 TCC/Saga 隔离性问题,在分布式数据库里完全不是问题。
3. 运维统一
Seata 方案:业务库 + TC 集群 + 注册中心 + 配置中心 + 监控 + 对账系统
分布式数据库:数据库集群本身(把所有协调能力打包了)
4. 性能可能更好
- TCC 一个全局事务要 6-9 次 RPC
- TiDB 的跨节点事务通常几十毫秒
- 应用层的协调开销,可能比 DB 内部的协调开销还大
5. 业务关注业务
让业务程序员从"分布式事务专家"回归到"业务专家"——不用学 TCC、不用设计补偿逻辑、不用处理幂等和防悬挂。
选型时要注意的工程细节
1. 不是所有"分布式数据库"都一样
| 类型 | 代表 | 分布式事务支持 |
|---|---|---|
| NewSQL | TiDB / CockroachDB / OceanBase / Spanner | ✅ 完整 ACID,跨节点强一致 |
| MySQL 集群 | MySQL Group Replication / Galera | ⚠️ 有限支持,主要是同步复制 |
| 分库分表中间件 | ShardingSphere / Mycat | ❌ 不是真正的分布式事务 |
| NoSQL | MongoDB / Cassandra | ⚠️ 部分支持,限制多 |
关键点:你要的是 NewSQL。分库分表中间件看起来像统一数据库,但跨分片事务依然有问题。
2. 跨节点事务依然比单节点贵
单分片事务:1-2ms
跨分片事务:10-50ms(TiDB Percolator)
业务设计依然要尽量让事务在单分片内完成——合理选择分片键。
3. 迁移成本不可忽视
如果已经在用 MySQL + Seata,迁移到 TiDB 不是零成本:数据迁移、SQL 兼容性测试、性能测试、团队学习成本、机器成本。新项目用 TiDB 没问题,老项目改造要算总账。
4. 运维要求高
业务代码简单了,但数据库本身的运维变复杂:TiDB 集群至少 3 个节点,需要懂 PD、TiKV、TiDB Server。小团队可能 hold 不住。
5. 成本问题
单机 MySQL:1 台机器
TiDB 最小集群:6 台机器
中小业务可能用不到分布式数据库的能力,为了避免分布式事务而部署 TiDB 不划算。
现实决策矩阵
| 场景 | 推荐方案 |
|---|---|
| 新项目 + 数据量大 + 团队有 DBA | TiDB / OceanBase,一步到位 |
| 新项目 + 数据量中等 + 简单业务 | 单机 MySQL,等需要再考虑分布式 |
| 老项目 + 业务稳定 + 已用 Seata | 保持现状,迁移成本不划算 |
| 老项目 + 痛苦的 Seata 改造 | 评估迁移到 TiDB |
| 混合场景(自有库 + 外部 API) | 必须用 TCC/Saga,DB 解决不了跨外部服务 |
| 跨多个独立 DB(如 MySQL + Redis) | 必须用 Seata 这类方案 |
一个正在发生的趋势
越来越多的新项目直接用分布式数据库(TiDB/CockroachDB),跳过了 Seata 这一代方案。
阿里自己也在大规模用 OceanBase 替换 MySQL + Seata 的组合。原因就是——当数据库能搞定,没必要在应用层重复造轮子。这也是为什么国内对 Seata 的热度近两年在下降。
但要警惕一个误区
"用了 TiDB 就再也不用考虑分布式事务" —— 错。
即使在 TiDB 内部:
- 跨服务的业务流程依然要考虑事务边界
- 长流程业务依然适合 Saga(不是因为一致性,是因为流程编排)
- 包含外部调用的事务(发短信、调支付)依然要补偿设计
- 大事务依然要避免(锁太多影响并发)
分布式数据库消除的是"协调复杂度",不是"业务复杂度"。
核心结论
在"应用分布式但数据库统一"的场景下,分布式数据库是更优解——它把所有 TCC/Saga/Seata 的复杂度,重新打包成了普通 SQL + ACID 事务。
但选型要算总账:
- 新项目 + 有运维能力 → 直接用分布式数据库
- 老项目 + Seata 跑得好 → 不必迁移
- 数据规模小 → 单机 MySQL 就够,别为了"避免分布式事务"而过度设计
分布式事务正在从"应用层问题"变回"数据库层问题"——Seata 这一代方案是过渡产物,未来会被原生分布式数据库逐渐替代。但完全消失不会——只要还有跨服务、跨外部系统的场景,应用层的协调就永远存在。
总结
分布式事务的本质:用某种协调机制让一组本地事务最终达到业务一致的状态。
所有方案的核心差异:何时提交本地事务 / 失败时如何恢复 / 协调机制是什么。
| 选型口诀 | |
|---|---|
| 要强一致、规模不大 | 2PC / XA |
| 要强一致、性能好 | 分布式数据库 |
| 资金核心业务 | TCC |
| 长流程业务 | Saga |
| 互联网普通业务 | 消息事务 / 本地消息表 |
| 应用分布式 + 数据库统一 | 分布式数据库(TiDB 等) |
| 不知道选什么 | Seata AT + 对账兜底 |
记住三件事:
- 没有最好的方案,只有最适合的方案 —— 评估业务特征做取舍
- 对账是最后的防线 —— 任何方案都不是 100% 可靠
- 能不用分布式事务就不用 —— 优先考虑业务划分能否避免
