分布式事务与一致性落地
2PC/3PC · TCC/Saga · 消息最终一致 · Seata
本主题的底层路线(万变不离其宗):
分布式事务的底层 = 协调者(Coordinator) + 补偿 / 回滚 + 幂等(+ 可选共识)。
要么走「强一致两阶段锁」路线(2PC / XA:先把所有参与者锁住,统一提交或回滚);要么走「最终一致靠补偿与消息」路线(TCC / Saga / 可靠消息:每个本地事务先提交,错了再反向补偿)。一句话记住 没有银弹,只有取舍——你想要"立刻全局一致"就得付"锁与阻塞"的代价,你想要"高可用高性能"就得接受"短暂不一致 + 自己写补偿"。
0. 读前必读:分布式事务,就是把本地事务拆到多机
你写单机程序时,BEGIN; UPDATE 账户; UPDATE 库存; COMMIT; 一个事务就搞定了——数据库帮你保证"要么都成功、要么都回滚"。但当你做微服务、分库分表后,账户表和库存表不在同一个库,甚至不在同一台机器、同一个进程里。这时"原子的提交"这件事,数据库自己管不了了——因为没有一个进程能同时锁住两个库、同时决定提交还是回滚。
分布式事务要解决的,就是一句话:"多机、多库、多服务各自提交,但对外看起来像一个原子操作。"
本部分的认知顺序(底层优先):先理解"为什么难"(敌人是谁)→ 再看"强一致路线"怎么用协调者 + 两阶段锁硬刚(2PC/XA)→ 再看"最终一致路线"怎么用补偿 + 消息 + 幂等绕开(TCC/Saga/可靠消息)→ 最后看 Seata 如何把这些机制封装成开箱即用。读每一节都问自己:"这里谁是协调者?靠什么回滚?靠什么保证重复不炸?"
一句话记住 分布式事务 = "把本地事务的原子性,从单库扩展到多机";所有方案的本质区别,只是"谁、在什么时候、用什么手段"来兜底这个原子性。
1. 背景:为什么分布式事务这么难
1.1 一个订单-库存-账户的跨库故事
假设下单时要同时做三件事:
- 订单服务:在
order_db 插入一条订单(状态"已创建")。
- 库存服务:在
stock_db 扣减 1 件库存。
- 账户服务:在
account_db 扣减 100 元。
如果订单插入成功、库存扣减成功,但账户扣款时因为余额不足或网络断了失败——那前面两件"已经落库"的事怎么办?要么三个一起成功,要么一个都不留痕迹。这正是事务的原子性(Atomicity)要求。
单机事务靠数据库内部的 undo/redo 日志和行锁就能兜底;跨库后,没有一个中心能同时回滚两个库的已提交数据,于是"部分成功"的脏状态就成了必须专门解决的难题。
1.2 三个永恒敌人:分区、崩溃、消息丢失
分布式事务比本地事务难,根子在"物理世界的不可靠":
| 敌人 | 表现 | 对事务的含义 |
| 网络分区 / 延迟 | A 能连协调者,B 连不上;消息迟到、乱序 | 你不知道对方到底是"已提交"还是"刚要提交",状态无法确定 |
| 节点崩溃 | 协调者或参与者在"提交前最后一刻"宕机 | 重启后它失忆了:该提交还是该回滚?这种中间态最致命 |
| 消息丢失 / 重复 | 通知丢了一条;或同一条发了两遍 | 收不到回滚指令会脏写;收到两遍会重复扣款 → 引出"幂等" |
一句话记住 分布式事务的所有复杂性,都来自三个字:不可靠——网络会断、机器会挂、消息会丢或重。协议就是为"在不可靠上达成可靠"而设计。
1.3 ACID 在分布式的崩塌
本地事务的 ACID 四大性质,搬到分布式下各自受挫:
- A(原子性):最难。跨库没有统一提交点,只能靠"协调者 + 两阶段"或"补偿"来模拟。
- C(一致性):业务层面的"账户+库存永远对得上"在跨库中间态必然短暂破坏,只能最终恢复。
- I(隔离性):各自库的锁锁不住别人的库。A 扣库存时,B 的补偿可能正改同一行 → 脏写、丢失更新。
- D(持久性):单库能保,但"三个库都持久了才算成功"需要协同确认。
正因为这四大性质在分布式下很难同时满,业界才分化出两条路线:强一致(咬牙保住原子+隔离,付性能代价) 与 最终一致(先放你各自的原子,靠补偿+幂等兜底,换高性能)。
1.4 一致性谱系:强一致 vs 最终一致
这和你在本系列「分布式协调」里学的 CAP 直接衔接:
- 强一致(CP 倾向):事务完成时,所有参与者数据立刻一致、对外可见。对应 2PC / XA。代价是同步阻塞 + 锁资源。
- 最终一致(AP 倾向):允许中间态不一致,但通过补偿/消息保证"过一会儿一定一致"。对应 TCC / Saga / 可靠消息。代价是要自己写补偿、要处理中间态可见。
取舍 选哪条路线,本质是在问:"你的业务能不能容忍'短暂不一致'?" 银行转账(钱不能多不能少,哪怕一秒)偏向强一致;电商下单(库存暂时飘一下、稍后对齐)常走最终一致。
2. 强一致路线:2PC 两阶段提交
2PC(Two-Phase Commit)是分布式事务最经典、最"教科书"的强一致方案。它不神秘,核心就一句:先让所有参与者把活干一半并锁住(Prepare),确认大家都没问题后,再由协调者一声令下统一提交(Commit)。
2.1 核心思想:先问"能不能",再"统一做"
直觉类比:公司团建,老板(协调者)问三个同事(参与者):"下周六能去吗?"(Prepare)。三人都说"能"(Vote Yes),老板宣布"那就定了"(Commit);只要有一人说"去不了"(Vote No),老板宣布"取消"(Rollback)。关键点:在听到老板最终决定前,谁都不能擅自动——这就是"锁"。
2.2 阶段一 Prepare(准备/投票)
- 协调者给每个参与者发
Prepare(xid),xid 是全局事务 ID,用来把这次跨库操作串起来。
- 每个参与者:执行本地事务的所有 SQL,但不提交;把 undo(回滚用)和 redo(重放用)日志写好;对涉及的数据加锁(行锁/表锁),防止别人并发改。
- 然后向协调者回复
Vote Yes(我准备好了,可以提交)或 Vote No(我这边出错,干不了)。
注意:Prepare 阶段数据已经改了,只是"挂起未提交"。锁一直握着,直到第二阶段结束。这正是性能瓶颈的源头——锁被占用的时间 = 整个 2PC 的耗时(包含多次网络往返)。
2.3 阶段二 Commit / Rollback(提交/回滚)
- 全票 Yes → 协调者发
Commit;每个参与者正式提交本地事务、释放锁、回 ACK。
- 任一 No,或协调者超时没收到某人回复 → 协调者发
Rollback;每个参与者用自己 Prepare 阶段写的 undo 日志回滚、释放锁、回 ACK。
一句话记住 2PC 的"两阶段"=「先各自干一半并锁住、投票」+「听令统一提交或回滚」。它用"协调者这一个点"换来了"全局原子性"。
2.4 协调者:唯一的"裁判"
协调者(Coordinator / Transaction Manager)是 2PC 的灵魂,也是命门:
- 它掌握全局事务状态机:
开始 → 准备中 → 提交/回滚决策 → 完成。
- 它的决策一旦做出就不可逆(比如已决定 Commit,就必须让所有人最终都 Commit,哪怕有参与者暂时连不上也要重试到成功)。这是个持久化承诺。
- 它用一个事务日志记录"我已决定 Commit 这个 xid",宕机重启后照着日志把未完成的人补完——这是 2PC 容错的核心(所谓"对赌协议"的实现基础)。
2.5 五大痛点:阻塞/单点/不一致/脑裂/超时
| 痛点 | 解释 |
| ① 同步阻塞 | Prepare 到 Commit 之间,所有参与者都拿着锁干等。并发量大时锁冲突严重、吞吐低。 |
| ② 单点故障 | 协调者挂了,整个事务卡死,参与者锁永远不释放(除非协调者恢复)。 |
| ③ 数据不一致 | 协调者发完 Commit 给 A,自己宕机;B 没收到 Commit。结果:A 提交了、B 没提交 → 数据分裂。 |
| ④ 脑裂(双主) | 协调者宕机后选出新协调者,但旧协调者其实还活着(网络分区假死),两个"裁判"下不同指令,参与者无所适从。 |
| ⑤ 超时困境 | 参与者收不到协调者最终指令时,它不知道该提交还是回滚(可能别人都提交了),只能干等或赌——这就是 2PC 无法在"分区时"保证一致的根本原因。 |
2PC 的致命伤:协调者在"做出决策后、通知完所有人前"这个窗口宕机,会造成部分提交的不一致,且无法自动修复。这是它"强一致"名义下的真实裂缝——它只在"协调者不在这个窗口挂"时强一致。
2.6 图解 2PC 时序
图 1:2PC 时序。阶段一所有参与者执行并加锁但不提交;阶段二由协调者统一下令提交。右侧红框标注"协调者决策后、通知完前宕机"的不一致窗口——这是 2PC 的硬伤。
3. 3PC:给 2PC 打的三处补丁(仍非完美)
3PC(Three-Phase Commit)是想修补 2PC"参与者收不到最终指令就只能干等"的痛点。思路:把第二阶段再拆一刀,让参与者在超时时有更明确的"默认动作"。
3.1 CanCommit / PreCommit / DoCommit
- CanCommit(询问):协调者只问"你能参与吗?"(不涉及加锁,轻量探测)。参与者回 Yes/No。
- PreCommit(预提交):只有全票 Yes 才进入。协调者发 PreCommit,参与者执行本地事务、写日志、加锁但不提交(等于 2PC 的 Prepare)。参与者回 ACK。
- DoCommit(提交):协调者收齐 ACK 后发 DoCommit,参与者正式提交、释放锁。若协调者超时,参与者在 PreCommit 阶段已拿到"大家都准备好了"的信号,默认自行提交(这是和 2PC 最关键的差别)。
3.2 超时推进与单点缓解
- 超时即推进:参与者在 PreCommit 后若迟迟等不到 DoCommit,假定协调者已决定提交而自行提交——避免了 2PC 里"干等到死"的阻塞。
- 单点减弱:因为多了一个明确的"预提交已达成"中间状态,协调者崩溃后新协调者能从参与者的状态推断出"该提交还是回滚",缓解脑裂。
3.3 为什么依然可能不一致
3PC 仍然不是完美的:如果参与者在 PreCommit 后、DoCommit 前,因为网络分区收不到 DoCommit,它"自行提交"了;但协调者其实是因为收到某个参与者的 No 才决定回滚的——于是这个自行提交的参与者,和回滚的参与者 again 出现不一致。3PC 只是把"不一致的概率和等待时长"降低了,没有根除。它需要"网络分区期间节点行为可控"的假设,现实难保证。
一句话记住 3PC = 2PC 加一个"预提交"中间态,让参与者超时后能"按默认动作"推进,减少阻塞;但网络分区下仍可能不一致,所以工程上 2PC/XA 远比 3PC 常用。
4. XA:数据库层的 2PC 标准接口
2PC 是个"思想",XA 是它在数据库层的标准化落地——定义了数据库(资源管理器)和事务管理器之间该怎么对话。
4.1 XA 规范与 xid
XA(eXtended Architecture,由 X/Open 组织定义)规定了一套命令接口,让"事务管理器(TM)"能驱动多个"资源管理器(RM,即数据库)"走 2PC:
xa_start(xid) -- 开启一个全局事务分支
... 业务 SQL ...
xa_end(xid) -- 结束分支执行
xa_prepare(xid) -- 第一阶段:准备(对应 2PC Prepare)
xa_commit(xid) -- 第二阶段:提交
xa_rollback(xid) -- 第二阶段:回滚
xa_recover() -- 崩溃恢复:列出"已 prepare 但未决"的事务
xid 是全局事务 ID,把分布在多个库的同一笔业务串到一起。数据库底层用 undo 日志实现"prepare 后不提交、rollback 可回退"。
4.2 JTA:Java 的事务 API
在 Java 里,JTA(Java Transaction API)是 XA 的上层接口;具体落地靠JTS 或应用服务器/框架(如 Spring + Atomikos / Narayana)充当事务管理器(TM),去调用各数据库的 XA 驱动(RM)。
@Transactional // Spring 在背后:开启 JTA 全局事务
public void placeOrder() {
orderDao.insert(...); // 分支1:order_db (XA)
stockDao.deduct(...); // 分支2:stock_db (XA)
accountDao.pay(...); // 分支3:account_db (XA)
} // 方法结束 → TM 自动走 2PC:prepare 全部分支 → commit
对开发者来说,XA/JTA 的侵入性极低——你写法和本地事务几乎一样,提交回滚的"两阶段"由 TM 在背后自动编排。这是它最大的吸引力。
4.3 XA 的局限(锁定/跨语言/性能)
| 局限 | 说明 |
| 长事务锁资源 | Prepare 阶段就对数据加锁且不释放,直到全局提交。微服务调用链长 → 锁持有时间 = 整条链耗时 → 严重阻塞、易死锁。 |
| 强依赖数据库 XA 支持 | MySQL/PostgreSQL/Oracle 支持,但很多 NoSQL、消息队列、外部 API 不支持 XA → 无法纳入同一全局事务。 |
| 跨语言/跨协议难 | XA 是数据库协议级的,微服务里"调用一个远程 HTTP 服务"套不进 XA。 |
| 性能差 | 多次网络往返 + 长锁。高并发场景吞吐量远低于最终一致方案。 |
一句话记住 XA = "数据库帮你跑 2PC",开发爽(几乎零侵入)、但代价是长锁 + 慢 + 只能框住支持 XA 的资源。它适合"短平快、同构数据库、强一致"的内部场景,不适合长链微服务。
5. 最终一致路线之一:TCC(Try-Confirm-Cancel)
当 XA 的长锁你忍不了,就换思路:不靠统一锁,而是让每个服务先"预留资源",都预留成功再"确认",任一失败就"取消预留"。这就是 TCC。
5.1 三阶段语义:预留 → 确认 / 取消
| 阶段 | 语义 | 类比 |
| Try | 预留业务资源(不是真正完成)。如冻结库存、冻结金额,而非直接扣减。 | 餐厅订位:先"锁定"座位,你还没来。 |
| Confirm | 真正执行。使用 Try 阶段预留的资源,做真正的扣减/落库。必须成功(靠重试)。 | 你到店入座,座位归你。 |
| Cancel | 释放 Try 预留的资源。撤销冻结,回滚到之前状态。 | 取消订位,座位释放给别人。 |
关键差异 vs 2PC:Try 阶段每个服务是"本地提交"的(冻结记录已落库),没有跨库长锁;Confirm/Cancel 各自独立提交,失败就无限重试——所以叫"最终一致"。
5.2 业务侵入:三个方法都得自己写
TCC 的最大代价是强业务侵入:框架(如 Seata-TCC、Hmily)只负责"编排调用 Try/Confirm/Cancel 并保证都走到",但三个方法的具体逻辑你必须自己写。
// 账户服务,要写三个接口
boolean tryPay(userId, amount); // 冻结 amount,记录冻结流水
boolean confirmPay(xid, userId); // 把冻结转为真实扣减(幂等)
boolean cancelPay(xid, userId); // 解冻,删除冻结流水(幂等)
TCC 的"侵入"不是框架配置,而是你得重新设计数据模型:要加"冻结字段/冻结表",把"直接扣"改成"先冻结再确认"。这是它落地成本高的根。
5.3 空回滚(无 Try 先 Cancel)
场景:网络问题导致某个服务的 Try 没收到或没成功,但全局事务已经判定失败、开始发 Cancel。这时 Cancel 来了,可本地根本没 Try 过——这叫空回滚。
处理:Cancel 方法里要能识别"我压根没 Try 过",于是记一条"空回滚标记"并返回成功,不能报错。否则框架会以为 Cancel 失败而一直重试。
5.4 防悬挂(Try 晚到)
场景:上面那个没收到 Try 的服务,它的 Try 后来迟到了(网络延迟送达)。如果它真的执行了 Try,就会留下一笔"永远没人 Confirm/Cancel 的冻结"——资源被永久挂起,叫悬挂。
处理:Try 方法执行前,先查"有没有对应的空回滚标记"。如果有,说明全局已失败,Try 直接拒绝执行。这就是"防悬挂"。
5.5 幂等(Confirm / Cancel 重复)
网络会重发,Confirm/Cancel 可能被调用多次。方法必须幂等:用 xid + 分支id 做去重——执行前查"这笔录过没",录过就直接返回成功。TCC 三连坑(空回滚 / 防悬挂 / 幂等)是面试常考点,本质是"网络不可靠"在 TCC 上的三个具体表现。
一句话记住 TCC = 自己把"预留(Try)/确认(Confirm)/取消(Cancel)"三件套写好;空回滚(没Try别慌)、防悬挂(Try迟到要拒)、幂等(重复要扛)三坑都源于"网络会丢会重会慢"。
5.6 图解 TCC 三阶段与异常
图 2:TCC 正常走 Try→Confirm,失败走 Try→Cancel。下方红框是 TCC 必处理的三连坑:空回滚、防悬挂、幂等,根根都来自"网络会丢/会重/会慢"。
5.7 适用短事务
取舍 TCC 适合短事务、高并发、强一致诉求且资源可"预留"的场景(如秒杀扣库存、转账冻结)。不适合长流程业务(每个步骤都要写三套逻辑,成本爆炸),那该看 Saga。
6. 最终一致路线之二:Saga(长事务编排)
Saga 为长事务而生:一笔业务要跨很多步骤、跑很久(如"下单→扣库存→发货→通知→积分"),你不可能把它们都锁住。Saga 的思路:每一步都是独立的本地事务(立即提交),如果某步失败,就按相反顺序执行前面各步的"补偿"操作。
6.1 核心:一连串本地事务 + 对应补偿
把大事务拆成 T1 → T2 → T3 → ... → Tn,每个 Ti 是本地事务;为每个 Ti 配一个补偿 Ci(反向操作)。
- 全部成功:
T1 T2 T3 ... Tn,完事。
- 在 Tk 失败:向前补偿
Ck-1 ... C2 C1,把前面已提交的副作用逐一撤销。
例:下单(T1)→扣库存(T2)→扣款(T3);若扣款失败,则补偿:恢复库存(C2)→取消订单(C1)。
Saga 的"补偿"和"回滚"不同:回滚是数据库 undo,把没提交的数据抹掉;补偿是再发一笔反向业务(如"加回库存"),因为前面的 T 早已提交。所以补偿也要幂等。
6.2 编排 Choreography(事件驱动)
去中心、靠事件:每个服务完成本地事务后,发一个事件;下一个服务监听该事件,执行自己的事务,再发下一个事件。没有中心协调者。
- 优点:服务间松耦合,没有单点,易扩展。
- 缺点:流程分散在所有服务的事件里,"全局视图"难看;补偿链要每个服务自己知道"失败了该发什么补偿事件";调试像"听交响乐找谁走音"。
6.3 协同 Orchestration(中心协调器)
有中心、靠命令:一个编排器(Orchestrator)按顺序调用各服务,记住执行到哪一步;某步失败,编排器负责按记录反向调用各补偿。
- 优点:流程集中、易理解易监控、补偿逻辑一处管理。
- 缺点:编排器可能成单点(要自己高可用);服务间多了"被指挥"的耦合。
取舍 服务少、追求松耦合用编排(事件);流程复杂、要强管控用协同(编排器)。Seata 的 Saga 模式就是中心协同式。
6.4 隔离性缺失与对策
Saga 的阿克琉斯之踵:因为每步都立即提交,Ti 和 Tj 之间不存在数据库隔离。比如 T2 扣了库存并提交,这时另一个事务读了"已扣但未最终完成"的库存,或者 T3 失败后 C2 要恢复库存,但这段时间别人又买了——就会出"脏读/丢失更新"。对策:
| 对策 | 做法 |
| 语义锁 | 数据上加"处理中"状态位,别人看到就跳过重算,避免并发改同一行。 |
| 交换式更新 | 补偿用"加回确切数值"而非"设为原值",避免覆盖其间他人的修改。例:补偿写 stock = stock + 1 而非 stock = 100。 |
| 重读校验 | 更新前重读、比对版本/状态,发现已被别人动过就放弃或换路径。 |
| 业务兜底 | 实在难保隔离,就靠定时对账 + 人工介入收尾。 |
一句话记住 Saga = "把大事务拆成有补偿的小事务链";编排靠事件(松耦合)、协同靠编排器(好管控);它的软肋是没有隔离,要用"语义锁/交换式更新/重读"自己补。
6.5 图解 Saga 补偿(编排 vs 协同)
图 3:左为编排式(服务间事件接力,无中心);右为协同式(编排器统一指挥调用与补偿)。两者补偿链方向相反,都是"已提交步骤反向撤销"。
7. 最终一致路线之三:可靠消息驱动
还有一大类场景:"本地业务"和"要通知别人做的事"必须一起成功或一起失败,否则就丢消息。这就是"可靠消息最终一致"。
7.1 本地消息表(本地事务 + 消息表 + 投递 + 幂等)
最朴素也最可靠的方案,ebay 提出:
- 同一本地事务里:既执行业务 SQL,又往本地消息表插一条"待发送"消息(同库同事务,要么都成要么都败)。
- 写个定时任务扫描"待发送"消息,发给消息队列;发送成功就标"已发送"。
- 消费者拿到消息处理业务,处理完更新自己的去重/状态表,保证幂等。
- 若发送失败/没 ACK,定时任务重试投递,直到成功。
精髓:把"发消息"变成"本地落库一条记录",用数据库事务的原子性保证"业务做了就一定有消息记录",从而不丢消息。代价是多一张表 + 定时任务。
7.2 事务消息:RocketMQ 的 half + 回查
本地消息表要自己写定时任务,有点重。RocketMQ 把它做成了Broker 原生能力:
- 生产者发半消息(Half Message)给 Broker:对消费者不可见,仅占位。
- 生产者本地事务执行(下单)。
- 执行成功 → 发
Commit,半消息变可见;失败 → 发 Rollback,半消息丢弃。
- 回查(Check):若 Broker 迟迟没收到 Commit/Rollback(生产者宕机),会主动回调生产者查"这笔本地事务到底成没成",据此决定提交还是回滚。这是容错关键。
一句话记住 RocketMQ 事务消息 = "先发半消息占位 → 跑本地事务 → 再决定提交/回滚";回查机制堵住了"生产者提交前宕机"的漏洞。
7.3 最大努力通知(重试 + 对账)
最宽松的一种:不保证一定送达,但尽最大努力。生产者发消息,消费者处理;失败就按退避策略多次重试;同时提供对账接口,让接收方主动来查"我有没有漏掉"。典型用于"支付结果通知""短信通知"等允许最终补齐的场景。
取舍 本地消息表/事务消息 = "一定不丢"(强可靠);最大努力通知 = "尽量送到+对账兜底"(轻量、接受极小概率漏)。按业务对"丢失"的容忍度选。
7.4 图解 本地消息表流程
图 4:本地消息表。业务与消息记录在同一本地事务里落库,定时任务负责投递并标状态,消费方靠去重表幂等。核心是不依赖"发消息"的可靠性,而依赖"本地事务的原子性"。
8. 贯穿一切的基石:幂等设计
你会发现,TCC 的 Confirm/Cancel、Saga 的补偿、消息的消费——都反复要求"幂等"。因为分布式系统下,任何一次调用都可能在"已成功但未收到 ACK"时被重试。幂等就是"同一个请求来 N 次,效果和第 1 次一样"。
8.1 为什么分布式事务离不开幂等
没有幂等,补偿会被重复执行(库存加回两次)、消息会被重复消费(扣款两次)。所以"补偿/消息 + 幂等"是绑定的——你选最终一致路线,就等于默认要写幂等。这是底层机制的一部分,不是可选项。
8.2 唯一键 / 去重表
最常用:用 唯一业务键(如 order_id)建唯一索引。重复插入直接抛"已存在"异常 → 捕获后当作成功。或建一张去重表,处理前先 INSERT 记录,唯一键冲突即说明处理过。
INSERT INTO dedupe(record_id, xid) VALUES (?, ?); -- 唯一冲突=已处理,直接返回成功
-- 再做真正的业务
8.3 Token 机制
防"重复提交/重复下单":客户端先向服务端申请一个一次性 Token;提交时带上 Token,服务端用"Token + 业务"做唯一键消费一次后作废。重复带着同一 Token 来 → 直接拒绝。常见于表单防重、支付防重。
8.4 版本号 / 状态机
- 版本号(乐观锁):更新时带
WHERE version = 旧值,并发重复更新只有第一个生效(version 变了,后续更新影响行数=0 → 视为已处理)。
- 状态机:业务有"已创建→已支付→已发货"等状态;补偿/确认时先判断"当前状态是否允许这笔操作",非法状态直接忽略。比单纯去重更贴近业务语义。
一句话记住 幂等四板斧:唯一键/去重表(拦重复插入)、Token(拦重复提交)、版本号(乐观锁拦并发)、状态机(按业务语义拦非法)。最终一致路线缺了它必出 bug。
9. Seata:把底层机制封装成"开箱即用"
Seata(阿里开源)的价值:把前面讲的底层机制(协调者、补偿、全局锁、undo log、幂等)打包成框架,让你少写大量样板代码。它定义了一套统一的角色模型和四种模式。
9.1 角色:TC / TM / RM
| 角色 | 全称 | 职责 |
| TC | Transaction Coordinator | 协调者。独立部署的服务,维护全局事务和分支状态,驱动提交/回滚。对应 2PC 里的协调者。 |
| TM | Transaction Manager | 定义全局事务的边界(@GlobalTransactional 注解的方法),告诉 TC 开事务、决定最终提交还是回滚。 |
| RM | Resource Manager | 各业务库侧的代理,管理分支事务,向 TC 注册分支、上报状态、执行 TC 下发的提交/回滚。 |
看到没?TC/TM/RM 就是 2PC 的"协调者 + 应用 + 资源管理器"的现代化重命名。Seata 的骨架就是 2PC,只是把协调者独立成一个高可用服务,并加了"自动补偿"等增强。
9.2 AT 模式:一阶段提交 + undo log + 全局锁
AT(Auto Transaction)是 Seata 的默认、最省心模式,目标是"像写本地事务一样写分布式事务"。原理:
- 一阶段(自动提交):拦截你的业务 SQL,在执行业务前解析 SQL、生成undo log(含 before image 数据快照和 after image);执行 SQL;向 TC 申请并持有"全局锁"(对修改的数据加一把 Seata 管理的分布式锁);然后本地事务直接提交(不像 2PC 那样挂着)。
- 因为一阶段就提交了,本地锁释放得快,性能远好于 XA 的长锁。
9.3 AT 二阶段:自动回滚与脏写防护
- 全局提交:TC 通知各 RM"可以了",RM 异步删除 undo log 即可(本地数据早已提交,无需动作)。极其轻量。
- 全局回滚:TC 通知 RM 回滚;RM 用 undo log 里的 before image 反向生成补偿 SQL,把数据改回原样;再用 after image 做校验,确认中间没被别人改过。
- 全局锁防脏写:如果另一个非 Seata 事务(或别的全局事务)想改同一行,它拿不到 Seata 的全局锁 → 被阻塞/失败,从而避免"AT 还没回滚,别人已经改了新值,导致回滚把别人的修改也覆盖掉"的脏写。
一句话记住 Seata AT = "一阶段直接提交 + 偷偷留 undo log + 抢全局锁;二阶段提交就删日志、回滚就靠 undo log 反向补偿"。它用"留快照 + 抢全局锁"绕开了 XA 的长锁,代价是多了 undo log 表、且只拦 Seata 参与的写。
9.4 TCC / Saga / XA 模式
Seata 不止 AT,还内置了另外三种,正好对应我们前面学的底层路线:
| Seata 模式 | 对应底层 | 特点 |
| AT | 自动补偿(undo log + 全局锁) | 零代码侵入,自动;适合大部分 CRUD 场景 |
| TCC | TCC 三阶段 | 你要写 Try/Confirm/Cancel;适合高性能、需预留资源 |
| Saga | 长事务补偿链 | 中心编排器驱动;适合长流程、跨多服务 |
| XA | 数据库层 2PC | 直接用数据库 XA;强一致但长锁,慎用于长链 |
框架即机制 这再次印证本系列铁律:框架只是底层机制的实例化与取舍。Seata 四种模式 = {自动补偿, TCC, Saga, XA} 四种底层机制,被同一个 TC/TM/RM 骨架串起来。
9.5 图解 Seata AT 一/二阶段
图 5:Seata AT。RM 拦截 SQL 留存 undo log、抢全局锁后本地直接提交(一阶段);二阶段提交仅删日志、回滚则靠 undo log 反向补偿。全局锁专门防"别人在回滚前改了这行"的脏写。
10. 选型对比:没有银弹,只有取舍
学了这么多,最后落到"我该用哪个"。记住 route 区的核心:要强一致就付锁/性能的代价,要高性能就接受最终一致 + 自己写补偿。
10.1 四个维度:一致性/侵入性/性能/复杂度
- 一致性强度:强一致(2PC/XA/Seata-XA)vs 最终一致(TCC/Saga/消息)。
- 业务侵入性:XA/Seata-AT 低(像本地事务);TCC/Saga 高(要写补偿/三方法)。
- 性能/吞吐:XA 最差(长锁);AT 较好(一阶段提交);TCC/Saga/消息 高(无跨库长锁)。
- 实现复杂度:2PC 协议最复杂(容错难);消息最终一致较易理解;TCC 三坑、Saga 隔离最磨人。
10.2 横向对比表
| 方案 | 一致性 | 侵入性 | 性能 | 隔离性 | 典型场景 |
| 2PC / XA | 强一致 | 低 | 差(长锁) | 好(锁住) | 同构DB、短事务、强一致内部系统 |
| Seata-AT | 最终/读已提交 | 极低 | 较好 | 中(全局锁防脏写) | 通用微服务 CRUD,想少写代码 |
| TCC | 最终(业务可见一致) | 高(三方法) | 高 | 弱(需自己设计) | 高并发、可预留资源(秒杀/转账) |
| Saga | 最终一致 | 中高(补偿) | 高 | 弱(无隔离) | 长流程业务(下单到履约全链) |
| 可靠消息 | 最终一致 | 中(消息表/事务消息) | 高 | 弱 | 解耦通知、异步补齐(支付通知) |
10.3 图解 选型定位
图 6:选型定位四象限。横轴业务侵入/复杂度,纵轴一致性强度(上强下弱)。没有方案在"右下强一致低侵入"——强一致必付代价,这正是"没有银弹"的几何表达。
10.4 决策清单
选型决策清单(自顶向下问):
- 能接受"短暂不一致"吗?不能(钱、核心账)→ 2PC/XA 或 Seata-XA,接受慢。
- 能接受,且想少写代码 → Seata-AT(通用 CRUD 首选)。
- 高并发、资源可预留(库存/额度)→ TCC。
- 流程长、跨多服务 → Saga(协同式好管控)。
- 只是异步通知/解耦补齐 → 可靠消息(本地消息表 / RocketMQ 事务消息)。
- 无论选哪条最终一致路线,幂等都是必答题。
11. 一句话路线总结
一句话记住 分布式事务的底层只有一条路被切成两端:一端是"协调者 + 两阶段锁"(2PC/XA,强一致但慢),一端是"补偿 + 消息 + 幂等"(TCC/Saga/可靠消息,快但需自己兜底)。Seata 是把这两条路都修成了"框架高速公路"。先想透协调者/补偿/幂等这三个词,所有框架你都能一眼看穿它用的是哪块积木。
回到底层路线:下次看到任何分布式事务框架,先问三句——① 谁是协调者?(TC / 中心编排器 / 消息 Broker)② 靠什么回滚?(undo log / 补偿链 / 反向业务)③ 怎么保幂等?(去重表 / 状态机 / 全局锁)。答得上来,框架就不再是黑盒。