分布式事务:跨服务 / 跨库怎么保证一致
分布式事务:跨服务 / 跨库怎么保证一致
"扣库存 + 扣余额"在单库里一个事务就能搞定:
BEGIN到COMMIT,要么全成、要么全回滚。可一旦拆成两个服务、两个库,数据库事务只管得了自己那一个连接——"要么都成功、要么都回滚",突然成了一道难题。这篇把主流方案理一遍:从 2PC,到互联网真正在用的 TCC / Saga / 可靠消息,以及一个反直觉的结论:最好的分布式事务,是想办法不用分布式事务。
一、是什么 & 为什么难
是什么。 分布式事务,指的是一次业务操作,横跨了多个独立的数据源(多个数据库、或多个各管一个库的服务),但业务上要求它们像一个事务一样——要么全部成功,要么全部回滚,不能出现"库存扣了、余额没扣"这种半截状态。
为什么难,先看单机为什么不难。 单机事务能保证 ACID(原子性、一致性、隔离性、持久性,见《数据库事务与隔离级别》篇),靠的是一个数据库引擎在背后统一调度:所有改动写在同一份 redo / undo 日志里,提交就一起生效,回滚就一起撤销,锁也由它统一管。关键前提是"一个引擎说了算"。
拆开之后,这个前提没了。 库存在 A 库、余额在 B 库,A 的事务提交了,B 的事务还能不能提交,A 根本不知道;等 A 发现 B 失败了想反悔,A 这边可能早就 COMMIT 落盘、别人都读到了——本地事务的"回滚"能力,出不了自己这台机器。更要命的是,A 和 B 之间隔着网络,而网络会延迟、会超时、会丢包:你给 B 发了"提交",没收到回复,到底是 B 没收到、还是 B 提交完了但回复丢在路上? 你区分不了。
所以分布式事务的难,本质是两件事叠加: 一是没有一个统一的协调者天然存在(得自己造一个);二是网络不可靠,任何一步都可能"结果未知"。这也是为什么严格的强一致(CAP 里的 C)在分布式下代价极高——这部分理论在《分布式理论》篇展开,这里只需记住:分布式一致性不是"加个事务"就有的,它是要专门花代价去换的。
一句话点破:单机事务是数据库白送的,分布式事务是你自己掏钱买的——而且不便宜。
二、强一致方案:2PC / 3PC
既然缺一个统一协调者,最直接的思路就是造一个。这就是 2PC(两阶段提交)。
是什么(2PC)。 引入一个协调者(Coordinator),把各个数据源当作参与者(Participant),提交分成两个阶段:
- 第一阶段——准备(Prepare):协调者问所有参与者"这笔操作你能不能提交?"。每个参与者执行操作、写好日志、锁住相关资源,但先不提交,只回一句"我准备好了(Yes)"或"我不行(No)"。
- 第二阶段——提交 / 回滚(Commit / Rollback):只要有一个参与者说 No,协调者就通知所有人回滚;全员都说 Yes,才通知所有人正式提交。
它的逻辑很优雅——但问题也很硬。
- 同步阻塞:从"准备"到"提交"这段时间,所有参与者都得锁着资源干等协调者发话。参与者越多、网络越慢,锁就握得越久,并发直接被压下来。
- 协调者单点:协调者一旦在"准备"之后、"提交"之前挂掉,所有参与者就卡在"锁着资源、等指令"的状态,谁也不敢动——整个事务悬住。
- 数据不一致的窗口:第二阶段发"提交"时,如果只有一部分参与者收到了(另一部分网络抖了),就会出现一半提交一半没提交,还得靠超时和重试去补救。
3PC 是 2PC 的改良:在中间多插一个"预提交(Pre-Commit)"阶段,并给参与者加了超时机制,缓解了"协调者挂了就死等"的阻塞,但代价是多一轮网络交互、更慢,而且并没有彻底解决一致性问题。所以 3PC 多停留在理论层面,实际很少用。
现状:它存在,但不在高并发链路上。 2PC 的工业标准叫 XA(一套分布式事务接口规范,主流数据库和 JTA 都支持)。它在强一致要求高、并发不大的传统场景(典型如银行内部跨库、企业级 ETL)还有用武之地;但在互联网那种高并发、长链路的业务里,它那把"长时间锁资源"的同步阻塞,基本是性能上不可承受的。于是互联网普遍转向了下面这套思路。
三、柔性事务(最终一致,互联网主流)
强一致太贵,那就退一步:不再要求"任何时刻都一致",而是允许中间有一小段不一致,但最终会达成一致——这就是最终一致性(Eventual Consistency)(理论出处见《分布式理论》篇),对应的工程手法统称柔性事务。它的核心思想是:不锁资源死扛,而是先各自提交本地事务,出了问题再用"补偿"把它补回来。 主流有三种。
1)TCC(Try-Confirm-Cancel)。
是什么。 把一个操作拆成三个动作,由业务自己实现:
- Try:预留资源(不是真正执行)。比如下单时,不直接扣库存,而是把这部分库存冻结起来。
- Confirm:全链路 Try 都成功后,确认执行(把冻结的库存真正扣掉)。
- Cancel:有任何一步失败,取消并释放预留(把冻结的库存解冻还回去)。
场景。 资金、库存这类对一致性敏感、又扛不住长时间锁库的场景——比如下单要同时扣库存、扣账户余额、加积分,跨了好几个服务。TCC 用"先冻结、再确认"换掉了 2PC 的"锁资源",锁的粒度从数据库行锁变成了业务层的预留,并发能力强很多。
注意(TCC 的代价很实在)。 它要求每个参与方都写 Try / Confirm / Cancel 三个方法,改造成本高,业务侵入强。还有两个经典坑必须处理:
- 空回滚:Try 还没执行(或请求都没到),却先收到了 Cancel——Cancel 得能识别"根本没 Try 过",直接当成功返回,不能真去释放一笔不存在的预留。
- 悬挂:Cancel 比 Try 先到(网络乱序),Cancel 执行完了,迟到的 Try 才到——这个迟到的 Try 绝不能再去预留资源,否则这笔资源就永远挂在那里没人 Confirm 也没人 Cancel 了。
- 配套地,Confirm / Cancel 必须幂等(重试多次结果一致)——幂等是所有柔性事务的标配,见《消息队列》《接口设计》篇。
2)Saga。
是什么。 把一个长事务,拆成一串有序的本地事务 T1 → T2 → T3 …,每一步都立刻在本地提交(不留锁);一旦某一步失败,就反向依次执行补偿操作 C(n) → … → C2 → C1,把前面已经做完的步骤一个个补偿回去。它不预留、不冻结,走的是"做错了再倒着撤"的路子。
场景。 步骤多、链路长、耗时久的业务流程——典型如一个跨多个系统的订单履约:创建订单 → 扣库存 → 调用支付 → 通知物流。这种长流程用 TCC 要写一堆预留逻辑太重,用 2PC 锁太久根本不现实;Saga 让每步都快速落地、失败了再补偿,更适合长事务。它有两种编排风格:编排式(Orchestration),用一个中心协调器统一指挥每一步和补偿;事件式(Choreography),各服务通过事件互相触发,没有中心、更松耦合但链路更难追踪。
注意。 补偿不一定能"完美还原"(比如短信已经发出去了没法撤回,只能再发一条更正),所以 Saga 要求业务能接受补偿语义;而且因为每步都立刻提交,中间态对外是可见的(别人可能读到"订单已创建但还没支付"),业务设计上要容忍这种中间状态。
3)本地消息表 / 可靠消息最终一致。
是什么。 借助消息队列把"本地操作"和"通知下游"绑成一个最终一致的整体。最经典的实现是本地消息表:在同一个本地事务里,既写业务数据、又往一张消息表插一条"待发送消息"——这两件事在一个库里,本地事务保证它俩同生共死;事务提交后,再由一个后台任务把消息表里的消息投递到 MQ,下游消费它来完成自己那部分。投递失败就重试,直到成功。
为什么这么绕。 因为"提交本地事务"和"发消息给 MQ"是两个独立动作,直接先后做一定会有不一致窗口(事务提交了但消息没发出去 / 消息发了但事务回滚了)。本地消息表的巧妙之处,是把"要发消息"这件事本身,变成本地事务的一部分——只要业务提交了,消息就一定在表里跑不掉,剩下的交给"重试投递 + 下游幂等消费"去保证最终送达。(细节见《消息队列》篇的可靠投递。)
场景与注意。 适合下游能异步、能接受短延迟最终一致的场景(发券、加积分、发通知、异步同步数据)。它的代价是实现链路长、要维护消息表和投递任务;而且全靠重试,所以下游消费必须幂等(同一条消息可能被投递多次)。不少消息中间件提供的"事务消息",本质就是把这套本地消息表的逻辑做进了中间件里。
四、业界怎么做
主流框架:Seata。 分布式事务这块,开源里绕不开 Seata(阿里开源、现已进入 Apache 孵化),它的特点是把上面几种模式都收进了一个框架,按场景选模式:
| Seata 模式 | 对应思路 | 一句话 |
|---|---|---|
| AT | 自动补偿的 2PC 变体 | 无侵入——框架自动记录数据快照、自动生成反向 SQL 补偿,业务几乎不用改代码,最常用 |
| TCC | 上文的 TCC | 自己写 Try/Confirm/Cancel,侵入强但可控,适合资金类 |
| Saga | 上文的 Saga | 长流程、多步骤的状态机编排 |
| XA | 标准 2PC / XA | 依赖数据库 XA 能力,强一致、并发低 |
其中 AT 模式是 Seata 的招牌:它在第一阶段执行业务 SQL 时,自动把改动前后的数据快照记下来,正常就直接提交(不像 XA 那样长时间锁),失败时用快照自动生成反向 SQL 回滚——相当于把 TCC 的"补偿"做成了框架自动化,用起来接近无侵入,这也是它在 Java 微服务里流行的原因。
怎么选(没有银弹,看场景权衡)。
- 强一致、并发不高、传统跨库 → XA / 2PC。
- 资金 / 库存等敏感、要强控制 → TCC(或 Seata TCC)。
- 长流程、多步骤、能接受中间态 → Saga。
- 下游可异步、只要最终到达 → 本地消息表 / 可靠消息。
- Java 微服务、想少改业务代码 → 优先看 Seata AT。
一句话点破:框架解决的是"怎么做",但"该不该做、做到多强"得你自己按业务定——这恰恰是下一节最该说的事。
五、注意事项
- 别一上来就追求强一致——它的代价是巨大的。 强一致(2PC/XA)意味着长时间锁资源、低并发、协调者单点风险;绝大多数互联网业务根本不需要任何时刻都一致,只需要"最终一致 + 用户无感"。先问一句:这个场景真的不能容忍几秒钟的不一致吗? 大多数时候答案是"能"。
- 最好的分布式事务,是不用分布式事务。 这是业界的真共识。能用一个本地事务搞定的,就别拆成两个库;设计服务边界时,尽量让"必须一起成功或失败"的数据,归同一个服务、同一个库管(也就是按业务能力划清边界)。很多分布式事务的难题,其实是服务 / 库拆得不合理自己造出来的——拆之前先想清楚一致性边界,比事后用框架补救划算得多。
- 幂等是所有柔性事务的标配,不是可选项。 柔性事务全靠"重试"兜底,而重试必然带来重复执行,所以每个会被重试的环节(Confirm/Cancel、消息消费、补偿)都必须幂等——同一操作做一次和做十次,结果一样。幂等怎么做见《消息队列》《接口设计》篇,这里只强调:没做幂等的柔性事务,等于埋了颗重复扣款的雷。
- 一定要有对账兜底。 再周全的方案,在网络极端情况下也可能漏掉某笔补偿、卡住某条消息。所以资金、库存这类关键链路,必须配一套离线对账:定时把各方数据拉出来比对,发现不一致就告警、人工或自动修复。对账是分布式一致性的最后一道安全网——别指望某个框架能 100% 兜底。
- 复杂度要算进成本。 每引入一种分布式事务方案,都是给系统加了一层得长期维护的复杂度(三个方法、补偿逻辑、消息表、对账任务……)。它和缓存、微服务一样,是用复杂度换一致性的权衡——上之前先掂量:这笔复杂度,业务扛得起、也确实需要吗?
六、一张表:四种主流方案怎么选
把全文收进一张表——选型时对着这张表问"我的场景落在哪一行":
| 方案 | 一致性 | 复杂度 / 侵入 | 并发 | 适用场景 |
|---|---|---|---|---|
| 2PC / XA | 强一致 | 中(依赖 DB XA) | 低(同步阻塞、长锁) | 强一致要求高、并发不大的传统跨库(如企业级、银行内部) |
| TCC | 最终一致(控制力强) | 高(写三个方法 + 处理空回滚/悬挂) | 高 | 资金 / 库存等敏感、要强控制的短链路 |
| Saga | 最终一致 | 中高(写补偿、容忍中间态) | 高 | 步骤多、链路长的长事务(订单履约、多系统流程) |
| 本地消息表 / 可靠消息 | 最终一致(异步) | 中(消息表 + 投递任务 + 幂等) | 高 | 下游可异步、能接受短延迟(发券、加积分、通知) |
一句话收尾:这四种没有最优解,只有最贴合你场景的那个权衡;而比"选哪个"更重要的一步,是先想想——能不能把它们都省掉。
名词解释
- 分布式事务(Distributed Transaction):一次业务操作横跨多个独立数据源(多库 / 多服务),但要求它们要么全部成功、要么全部回滚的一致性问题。
- 2PC(Two-Phase Commit,两阶段提交):靠一个协调者,把提交分成"准备 → 提交/回滚"两阶段的强一致协议;缺点是同步阻塞、协调者单点。
- 3PC(三阶段提交):2PC 的改良,中间加"预提交"阶段并引入超时,缓解阻塞但更慢,多停留在理论。
- XA:一套分布式事务的工业标准接口规范,主流数据库和 JTA 支持,是 2PC 的标准实现;强一致但并发低。
- 协调者 / 参与者(Coordinator / Participant):2PC 里统一发指令的角色 / 各个被协调的数据源。
- 柔性事务:不追求时刻强一致,而是允许短暂不一致、最终达成一致的一类工程手法(TCC / Saga / 可靠消息等)。
- 最终一致性(Eventual Consistency):允许中间有一小段不一致,但保证最终所有副本 / 数据源达成一致(理论见《分布式理论》篇)。
- TCC(Try-Confirm-Cancel):把操作拆成"预留(Try)→ 确认(Confirm)→ 取消(Cancel)"三步、由业务实现的柔性事务模式。
- 空回滚 / 悬挂:TCC 的两个经典异常——Try 没执行就先收到 Cancel(空回滚);Cancel 比 Try 先到、迟到的 Try 不能再预留(悬挂)。
- Saga:把长事务拆成一串本地事务,失败时反向逐个执行补偿的模式;分编排式(中心协调)和事件式(事件驱动)两种。
- 补偿(Compensation):对已提交的本地操作做"反向操作"以撤销其影响(如解冻库存、退款),柔性事务回滚的实现方式。
- 本地消息表:在同一个本地事务里同时写业务数据和"待发送消息",再由后台任务可靠投递到 MQ,保证"业务提交"和"发消息"同生共死。
- 可靠消息最终一致:借消息队列的可靠投递 + 下游幂等消费,达成跨服务最终一致的方案(本地消息表是其经典实现,详见《消息队列》篇)。
- 幂等(Idempotent):同一操作执行一次和执行多次,结果完全相同;是柔性事务靠重试兜底的前提。
- 对账:定时把各数据源的数据拉出来比对,发现不一致就告警 / 修复,是分布式一致性的最后一道安全网。
- Seata:开源分布式事务框架(阿里开源、Apache 孵化),把 AT / TCC / Saga / XA 多种模式收进一个框架,按场景选用。
- AT 模式:Seata 的招牌模式——自动记录数据快照、自动生成反向 SQL 补偿,业务几乎无侵入,Java 微服务里最常用。
本文属《研发都要懂的事》·服务端架构设计系列——分布式阶段的一致性难题。单机事务 / ACID 见《数据库事务与隔离级别》篇,可靠消息 / 幂等见《消息队列》篇,最终一致与 CAP 见《分布式理论》篇;本篇只回答"跨服务、跨库时,一致性怎么保证"。记住最反直觉、也最值钱的一条:先想能不能不用它。
评论(0)
登录后参与评论。
还没有评论,来抢沙发吧。

