你下单付了钱,页面显示成功,仓库却根本没扣库存。如果订单和库存记在不同的数据库里、各记各的,就可能出现这种「一个成了,另一个没成」的情况。这一期讲:数据分在不同的库里以后,怎么还能一起成功或一起失败——也就是分布式事务。
1. 为什么本地事务不够用了
同一个库里,一笔事务要么全部生效,要么全部撤回。可它的「提交 / 回滚」只对本库的数据有效:订单库回滚,管不到库存库。数据分到不同的库以后(分库分表的代价之一就是跨库事务变难),两个库各自提交,中间任何一步失败——库存不足、网络断、进程重启——就只剩一半生效。
Azure 架构中心的说法是:单个服务在一个数据库里能遵守 ACID,跨多个服务时就复杂得多,传统的 ACID 保证「aren’t directly applicable to multiple independently managed data stores」1。RocketMQ 文档也用电商下单举过反例:订单执行了,消息却没发出去;超时错误又没法可靠判断要不要回滚订单2。
2. 第一种,两阶段提交(2PC / XA):先准备、再统一生效
以 MySQL 8.4 为例,XA 事务让多个独立的事务资源参与同一个全局事务,「either all components must commit, or all components must roll back」3。
- 角色:事务管理器 TM(协调者,比喻里拿对讲机的调度员)+ 各个资源管理器 RM(每台 MySQL,比喻里听调度员指挥的驿站)。连到 MySQL 的客户端程序充当 TM3。
- 第一阶段:所有分支先 prepare,各 RM 把本分支的动作记到稳定存储里,并回报能不能提交(铅笔写好、货锁进待出库格子,再举绿牌或红牌)3。
- 第二阶段:全都能提交,TM 让所有分支提交;有一个不能,就让所有分支回滚(一起盖章 / 全部擦掉)3。
MySQL 里的语句大致是 XA START → 执行 SQL → XA END → XA PREPARE → XA COMMIT(或 XA ROLLBACK)。
代价:进入 PREPARED 状态的 XA 事务会一直保留,直到显式执行 XA COMMIT 或 XA ROLLBACK4,可以用 XA RECOVER 查出还悬着的事务。也就是说,投了「准备好」以后,参与者自己不能决定,只能等协调者;协调者挂了,它就悬着,相关数据一直被占着(好比「对讲机没声了,只能干等」)。RocketMQ 文档也定性地指出 XA 要锁住大量资源,并发低2。另外 MySQL 对 XA 还有复制过滤、binlog 分段写等限制,见手册4。
3. 第二种,TCC:先预留,再确认或取消
Seata 文档:TCC 是两阶段提交里资源管理器的一种「面向服务」的实现;Try 负责检查并预留资源,是第一阶段;Confirm 是第二阶段提交;Cancel 是第二阶段回滚,释放预留、恢复原状5。它不依赖底层数据库,能缩短资源锁定的时间(reduces resource lock holding time),但要业务系统自己实现 Try / Confirm / Cancel 三个动作,设计相对复杂5,还要处理幂等、空回滚、悬挂。
用驿站说:不让数据库一直锁着,改成业务自己贴封条——蓝站先在货上贴「已预留」封条,货还在架上但别人买不走(Try);之后还是由调度员(协调者,Seata 文档里由 TM 分两阶段协调)喊确认或取消:订单成了就撕封条真出库(Confirm),没成就撕封条、货照常卖(Cancel)5。一致性怎么说:Seata 的原话是由 TM 分两阶段协调,保证所有资源的最终状态一致——要么都提交,要么都回滚(简单说:最后要么都确认,要么都取消)5。本文不把 TCC 称为「强一致」:Try 之后、Confirm 之前,预留是业务层的,不是数据库锁。「防着同一个指令重复送来」,就是 Seata 说的幂等问题5。Pat Helland 把这种做法叫 tentative operation(暂定操作):对方先接受不确定,之后收到确认或取消6。
版本:Apache Seata(孵化项目)最新正式版 2.6.0(2026-01-28);2.7.0 目前还是 RC 预发布版,不是正式版7。
4. 第三种,Saga:先做,失败了再补偿
Saga 把一个事务拆成一串本地事务;某一步失败,就执行一系列补偿事务,抵消前面已经做过的改动1。组织方式有两种:编舞(choreography,各方发事件,没有中心)和编排(orchestration,有一个编排器,但它本身会成为故障点)1。
要诚实讲清的几点1:
- 没有内置的跨服务隔离,中间状态别人看得见;
- 已提交的改动不能真的回滚,只能用反向操作补;
- 补偿也可能失败;
- 各步骤要幂等。
用驿站说:蓝站先真把货出了库,后面红站失败,就把货「重新上架」——这是一个反向动作,不是时光倒流:已提交的改动不能回滚1。中间那段时间别人能看见货架空了(没有隔离);补偿本身也可能失败,要能重试且幂等1。Azure 还指出,可重试的步骤让 saga「eventually achieve a consistent state」,即最终一致1。Seata 2.x 也提供 Saga 模式7。另见 microservices.io 的总结:缺少自动回滚、缺少隔离8。
5. 第四种,可靠消息:先记下来,再保证送到(不自动撤回订单;永久失败要另外处理)
5.1 本地消息表(Transactional outbox,仅参考)
订单库在同一个本地事务里,同时写「订单」和「一条待发送的通知」;之后后台反复把通知投递给库存服务,直到对方确认9。这样就避免了「数据库改了、消息没发」的一半状态。下游要按订单号去重(幂等),因为通知可能重复送达。
5.2 RocketMQ 事务消息(半消息)
流程2:先发一条「半消息」(暂不投递)→ 生产者执行本地事务 → 提交 Commit 或 Rollback;broker 没收到结果时,会主动回查生产者这条半消息的状态。超时未决默认回滚半消息(具体时长见官方参数说明,本文不引数字)2。打个比方:条子交邮局暂存不送 = 半消息;「可以送了 / 作废」= Commit / Rollback;邮局回头问 = 回查;蓝站没处理好就再送 = 消费重试2。注意:「同一页账上同时写订单和条子」是本地消息表的做法(见 5.1,仅参考),不是 RocketMQ 事务消息;事务消息也不是「连调度员都不要」,RocketMQ 自述是「在普通消息上支持两阶段提交」。
一致性边界:它「ensures only final consistency」,也不保证下游消费结果和上游执行结果一致,下游业务必须自己保证正确处理2。版本:RocketMQ 最新 5.5.1(2026-08-20),文档按 5.x10。
6. 最终一致:Saga 和可靠消息都是
最终一致不是 Saga 或可靠消息独有的说法,这两种都是:中间会有一段时间订单和库存对不上,等补偿(Saga)或重送(可靠消息)做成了,最后再对上21。但不能说「最后一定对上」:Azure 明确写了补偿事务可能失败,系统会停在不一致状态,要靠监控和重试1。
7. 方案怎么选(对照)
| 方案 | 一致性 | 协调者 | 失败怎么办 | 代价 | 比喻 | 详见 |
|---|---|---|---|---|---|---|
| 两阶段提交(XA) | 全局事务也具备 ACID(MySQL 手册原话 “extends ACID properties up a level”) | 要(TM) | 准备阶段有人失败就全部回滚;已决定提交的,恢复后继续提交 | 锁多、并发低;协调者挂了会卡住 | 调度员、干等 | §2(语句、PREPARED、限制) |
| TCC | 最终状态要么都确认、要么都取消(Seata 原话;不称强一致) | 要(如 Seata) | Cancel 释放预留 | 业务三套动作,复杂;要防重复 | 封条 | §3 |
| Saga | 最终一致,无隔离 | 编排有 / 编舞无 | 反向补偿(可能失败) | 中间态可见、要幂等 | 重新上架 | §4 |
| 本地消息表 / 事务消息 | 最终一致 | 不需要跨业务的全局事务协调者(消息服务器负责半消息的提交和回查) | 重试 + 回查 | 有延迟、下游要幂等 | 条子、邮局回头问、重送、认单号 | §5 |
四格对比(一句话版)
| 方案 | 怎么做 | 代价 |
|---|---|---|
| 两阶段提交 | 先准备、再统一生效 | 货一直锁着,别人也买不走 |
| TCC(预留确认取消) | 先预留,再确认或取消 | 每个业务都得自己写预留、确认、取消这三套动作 |
| Saga(补偿事务) | 先做,失败了再补偿 | 中间那段时间,别人能看见货架空了 |
| 可靠消息 | 先记下来,再保证送到 | 通知一定发出去;临时失败靠重试,永久失败要另外设计补偿或人工处理 |
注意区分:可靠消息本身不自动撤回订单,它保证的是通知一定送出去;临时失败靠重试,重试到上限会进死信队列,永久失败(比如库存不足)需要业务另行设计补偿或人工处理。Helland 也指出:多数分布式事务系统里,一个节点故障会让提交卡住6。没有哪一种「最好」,看业务能不能接受短暂对不上。
Footnotes
-
Azure Architecture Center:Saga design pattern,https://learn.microsoft.com/en-us/azure/architecture/patterns/saga ,2026-10-15 抓取,2026-10-16 复核 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Apache RocketMQ 文档:Transaction Message,https://rocketmq.apache.org/docs/featureBehavior/04transactionmessage/ ,页面无日期,2026-10-15 抓取,2026-10-16 复核(机制描述;版本以 S6 为准) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
MySQL 8.4 Reference Manual, 15.3.8 XA Transactions,https://dev.mysql.com/doc/refman/8.4/en/xa.html ,2026-10-15 抓取,2026-10-16 复核 ↩ ↩2 ↩3 ↩4
-
MySQL 8.4 Reference Manual, 15.3.8.3 Restrictions on XA Transactions,https://dev.mysql.com/doc/refman/8.4/en/xa-restrictions.html ,2026-10-15 抓取,2026-10-16 复核 ↩ ↩2
-
Apache Seata 文档:TCC 模式,https://seata.apache.org/docs/user/mode/tcc/ ,页面无日期,2026-10-15 抓取,2026-10-16 复核(机制描述;版本以 S4 为准) ↩ ↩2 ↩3 ↩4 ↩5
-
Pat Helland, “Life Beyond Distributed Transactions: An apostate’s opinion”, ACM Queue 14(5), 2016-12-12,https://queue.acm.org/detail.cfm?id=3025012 ,2026-10-15 抓取 → 2020 年以前 仅参考 ↩ ↩2
-
Apache Seata GitHub Releases,https://github.com/apache/incubator-seata/releases ,2026-10-15 抓取(v2.6.0 发布于 2026-01-28;v2.7.0 为 RC) ↩ ↩2
-
microservices.io:Pattern: Saga(Chris Richardson),https://microservices.io/patterns/data/saga.html ,页面无日期,2026-10-15 抓取 → 仅参考 ↩
-
microservices.io:Pattern: Transactional outbox(Chris Richardson),https://microservices.io/patterns/data/transactional-outbox.html ,页面无日期,2026-10-15 抓取 → 仅参考(机制与 S5 交叉) ↩
-
Apache RocketMQ GitHub Releases,https://github.com/apache/rocketmq/releases ,2026-10-15 抓取(rocketmq-all-5.5.1,2026-08-20) ↩
幕后
这部片子完全由代码生成。
系列 · 分布式第 2/2 期