Theory · Microservice · Distributed Transaction
分布式事务
一个操作横跨多个服务/多个库,本地事务失效 —— 刚性事务太贵,工程答案是"柔性":最终一致
一句话本质
ACID 事务只在一个数据库内有效;跨服务后只能二选一:强一致(2PC 系,贵且慢)或最终一致(Saga/TCC/消息,工程主流)
方案光谱
2PC/3PC(强一致协议)· TCC(业务补偿)· Saga(长流程编排)· 本地消息表/事务消息(最终一致主流)· Seata AT(框架化 2PC 变体)
选型定式
能不用就不用(重新设计边界);能用消息就不用 TCC;钱相关才上 TCC/强一致——按业务容忍度选方案
这份 deck 回答"跨服务一致性怎么保证":从问题场景出发,理论铺垫(CAP/BASE),然后逐个方案拆解——2PC/3PC(协议层)、TCC(业务层两阶段)、Saga(长流程)、本地消息表与事务消息(最终一致主流)、最大努力通知、Seata AT(框架化),最后选型对比表与 QA。核心记忆点:每个方案的"坑"(2PC 阻塞、TCC 三坑、Saga 补偿设计)。
Problem
问题场景:下单为什么不能再是一个事务
单体时代
// 单库单事务:ACID 天然保证
BEGIN;
INSERT INTO orders ...; -- 订单
UPDATE stock SET n=n-1 ...; -- 库存
INSERT INTO payments ...; -- 支付单
COMMIT; -- 要么全成,要么全滚
一个 BEGIN/COMMIT 搞定——数据库帮你保证原子性。
微服务时代
// 三个服务三个库,事务失效
orderSvc.Create() // 库A: 订单落库
stockSvc.Deduct() // 库B: RPC 扣库存
paySvc.Prepare() // 库C: RPC 创建支付单
// 库C 挂了: A 已提交,B 已提交
// 谁来把 A、B 回滚? —— 本地事务管不到
RPC 调用无法纳入对方的事务边界;部分成功 = 数据不一致(有单无货/有钱无单)。
不一致的三种成因
① 部分失败:中间某步挂了;② 网络不可知:超时但对方可能已成功;③ 并发交错:两个请求交叉读写跨库数据
先问一个更根本的问题
"这里真的需要跨服务事务吗?"——很多场景重新划边界(把强相关数据放进一个服务)或改成单服务内聚合,问题直接消失。分布式事务是最后手段
一致性需求的分级
强一致(钱、库存超卖)→ 刚性方案或兜底校验;最终一致(积分、通知、统计)→ 消息/补偿方案;容忍丢失(日志类)→ 尽力而为
用下单场景建立直觉:单体一个事务搞定,微服务三个服务三个库后 RPC 无法纳入事务边界,部分成功即不一致。不一致三成因:部分失败、网络不可知、并发交错。两个升华:先质疑是否真的需要跨服务事务(重新划边界);再分级一致性需求(强一致/最终一致/尽力而为)——这决定了后面所有方案的选择空间。
Theory
理论基础:CAP → BASE → 刚性 vs 柔性
CAP 的正确打开方式
- C(线性一致)/A(分区可用)/P(容忍分区)——分区不可避免 P 必选,实际在 C/A 之间选
- 分区发生时二选一:C 系统拒绝部分请求保一致;A 系统继续服务容忍数据分歧,分区恢复后收敛
- CAP 只约束"分区时刻"(平时 C/A 不冲突,延迟维度属 PACELC);常见误用"我们选 CA"——工程上默认 P 存在
BASE:工程化的松弛
- Basically Available(基本可用):故障/高峰时允许降级(响应变慢、功能收窄)
- Soft state(软状态):允许存在中间状态("处理中"的订单)
- Eventually consistent(最终一致):停止写入后一段时间,副本收敛到一致
- BASE 是互联网大厂的事实哲学:用短暂不一致换持续可用与性能
刚性事务 vs 柔性事务
| 刚性(ACID 跨库) | 柔性(BASE) |
| 代表 | 2PC/3PC/XA | Saga/TCC/消息最终一致 |
| 一致时机 | 事务提交瞬间 | 一段时间后收敛 |
| 锁定资源 | 长锁定,吞吐低 | 不锁或短锁,吞吐高 |
| 复杂度位置 | 数据库/协调者 | 业务代码(补偿逻辑) |
| 适用 | 短事务、强一致刚需 | 长流程、互联网高并发 |
面试话术
"CAP 说明跨库强一致的代价,BASE 给出方向(允许中间态、最终收敛)。微服务的'分布式事务'多数不是搬 ACID,而是设计补偿与收敛机制——这是后面每个方案的本质。"
理论页三块:CAP 正确理解(P 必选,分区时选 C 或 A;纠正"选 CA"的误用;提 PACELC 补延迟维度)、BASE 三词(基本可用/软状态/最终一致)、刚性 vs 柔性对比表(锁定、时机、复杂度位置)。收尾话术把理论与后面方案串起来:分布式事务的本质是补偿与收敛机制的设计。
Two-Phase Commit
2PC:强一致的标准协议与它的三大缺陷
2PC 时序图两阶段:投票(prepare,参与者预执行并锁资源回 yes)与提交(全 yes 才 commit)。三大缺陷必须展开:同步阻塞(prepare 后锁着等,协调者挂了参与者傻等)、协调者单点、commit 消息丢失导致部分提交不一致。落点:MySQL XA 是 2PC 实现,互联网高并发很少直接用——引出柔性方案。
Three-Phase Commit
3PC:加了"准备阶段",仍然不完美
三阶段流程
- CanCommit:协调者先问"能提交吗"(轻量询问,不锁资源)
- PreCommit:全员 yes 后,参与者预执行+锁资源(相当于 2PC 的 phase1)
- DoCommit:协调者发最终提交
- 改进点:参与者 PreCommit 后等不到 DoCommit 就默认提交(非傻等),缓解协调者单点阻塞
依然没解决的问题(为什么业界少用)
- 网络分区下仍不一致:孤立参与者超时默认提交,协调者实际决定 abort → 分裂脑
- 默认提交的前提(能走到 PreCommit ≈ 全员同意)在分区下不成立
- 多一轮 RTT,延迟更高
- 结论:3PC 在理论上未达成共识安全,工程上几乎不用——分区容错场景大家改用 Paxos/Raft 类多数派协议做"提交决议"(如 Kafka 事务协调、etcd)
2PC vs 3PC 对比一句话
3PC = 2PC + CanCommit 询问阶段 + 参与者超时自治(默认提交)。修复"阻塞等待",引入"分区下默认提交的错误风险"。面试答法:改进点 + 残留缺陷,结论工程少用。
延伸:Paxos Commit(理论正解)
Gray & Lamport 2004《Consensus on Transaction Commit》:用 Paxos 复制协调者决策(2PC+Paxos),分区下仍能达成一致决议——代价是每事务多次多数派写,太贵。意义:2PC 病根是协调者单点,正解是协调者共识化,Spanner 据此设计。
3PC 三阶段(CanCommit 轻询问、PreCommit 预执行、DoCommit),改进是参与者超时默认提交,消除协调者单点阻塞;残留缺陷是分区下默认提交与 abort 决议冲突(脑裂),且多一轮 RTT。结论:工程少用,理论正解是 Paxos Commit(Gray & Lamport 2004),Spanner 据此设计。这段延伸是拉开档次的部分。
Try-Confirm-Cancel
TCC:把两阶段做进业务代码
三个阶段的业务语义
- Try:预留资源——订单"冻结库存"(库存减"可用"加"冻结")、账户"冻结金额",不产生真实变更
- Confirm:用预留资源完成真实变更(冻结划转);必须幂等(重试会重复调用)
- Cancel:取消释放——把冻结的资源退回;必须幂等 + 支持空回滚
- 协调:事务发起方依次 Try 所有参与方 → 全成功则 Confirm,任一失败则 Cancel 已 Try 的
TCC 三大坑(必背,面试区分度最高)
- 空回滚:Try 未到达而 Cancel 先到——Cancel 查无 Try 记录要直接返回成功并记录"已回滚",Try 迟到时拒绝执行
- 悬挂:Cancel 执行后,阻塞的 Try 迟到了——Try 检查"已回滚标记",拒绝执行(否则资源白白冻结)
- 幂等:Confirm/Cancel 会重试——必须按事务 ID 去重
- 解法统一:事务控制表(xid 状态机:tried/confirmed/cancelled)贯穿三个接口
优缺点
优点:不长期锁资源(Try 只做冻结标记)、隔离性较好(中间态对其他事务不可见)。缺点:业务侵入大——每个参与方要实现三个接口+状态表,开发成本约 3 倍;Confirm/Cancel 的一致性依赖重试最终成功(仍是最终一致的落地)。
适用与实现
适用:金融类短事务(扣款+入账),流程短(2-3 个参与方)。实现:Seata TCC(注解 @TwoPhaseBusinessAction)、ByteTCC、HMily;Go 生态常自研(事务表+状态机+定时重试)。参与方一多(>4)就该换 Saga/消息。
TCC 三阶段业务语义:Try 预留(冻结)、Confirm 确认(真实变更,必须幂等)、Cancel 释放(幂等+空回滚)。三大坑是必背区分点:空回滚(Cancel 先到直接成功并记标记)、悬挂(迟到 Try 检查标记拒绝)、幂等(xid 去重)——统一解是事务控制表状态机。优缺点与适用:不锁资源但业务侵入大,金融短事务适用,参与方多就换 Saga。
Saga Pattern
Saga:长流程的顺序执行 + 逆向补偿
Saga 两链图:正向 T1→T2→T3 依次本地事务提交,T3 失败触发逆向 C2→C1 补偿。两种驱动方式:编舞(事件订阅,无中心,简单流程)vs 协调(中央协调器,复杂流程主流)。三条铁律:Ti/Ci 都是本地事务、Ci 幂等可重试、补偿不了的步骤放流程末端。缺隔离性(中间态可见)是 Saga 与 TCC 的核心差异。
Transactional Outbox
本地消息表:最终一致的工程主流(必背)
解决的问题:双写不一致
// 问题:DB 写成功 + MQ 发送失败
// 或 MQ 发成功 + DB 写失败
UPDATE orders SET status=1;
mq.Send("order.created") // 若这里挂?
// 两个系统无法用一个事务包裹
业务写库与发消息是两个系统,不存在原子操作——把"发消息"也变成一条数据库记录,纳入同一个本地事务。
四步闭环
- ① 同事务写:业务数据 + 消息记录(status=pending)在同一个本地事务落库——原子
- ② 投递:后台任务/事务后钩子扫描 pending 消息发 MQ,成功置 sent
- ③ 确认:MQ ack 后标记完成(at-least-once,可能重复发)
- ④ 补偿:定时任务重投超时 pending;消费方幂等去重(msg_id 唯一索引)
为什么它靠谱(设计精髓)
把"不可靠的网络发送"转成"可靠的本地落库 + 可重试的异步投递":一致性锚点从网络挪到了本地事务。代价只是多一张表与一次扫描——没有新组件、没有协议、没有锁。这是"用最土的办法解决最难的问题"的典范,也是为什么它是业界实际用得最多的方案。
细节三问(追问预防)
① 扫描性能:消息表分片/归档,或用 binlog 订阅(Canal/Debezium CDC)替代扫描——"事务消息的 CDC 化"是现代演进;② 顺序性:同业务键分区/顺序消费;③ 消息表放哪个库:与业务同库(才能同事务),所以限流/归档要跟上。
本地消息表是必背方案:四步闭环——业务与消息记录同事务落库、后台扫描投递、ack 确认、超时重投+消费方幂等。设计精髓一句话:把不可靠的网络发送转成可靠的本地落库+可重试投递,一致性锚点挪到本地事务。追问三连:扫描性能(分片归档/CDC 替代)、顺序性、消息表与业务同库。
Transactional Messaging
事务消息:把"消息表"做进 MQ(RocketMQ 半消息)
RocketMQ 半消息机制
- ① half send:生产者先发"半消息"(消费者不可见)
- ② 本地事务:半消息发送成功后执行本地事务
- ③ commit/rollback:本地事务成功 → 提交半消息(消费者可见);失败 → 删除
- ④ 回查:若 broker 一直没收到确认,定时回查生产者"本地事务到底成没成"——兜底解决"提交确认丢失"
- 效果:等价于本地消息表,但扫描/重投/回查都由 MQ 基础设施代劳
与本地消息表对比
| 本地消息表 | 事务消息 |
| 依赖 | 只依赖 DB | 依赖支持事务消息的 MQ |
| 侵入 | 建表+扫描代码 | 实现回查接口 |
| 可靠性锚点 | 本地 DB 事务 | broker 半消息+回查 |
| 运维 | 自己管重投 | MQ 托管 |
| 注意 | CDC 化演进 | 回查接口也要幂等;Kafka 事务是流处理语义,非此模式 |
事务消息解决不了什么(边界感)
保证"本地事务成功 ⇒ 消息必达"(可靠投递),但不保证消费成功——消费失败靠重试 + 死信 + 人工;也不提供隔离性(下游看到消息时其他数据可能未同步)。要求"下游动作必须成功"的场景,再叠 Saga/对账。
消费端的最终一致闭环
可靠投递(本页)+ 可靠消费(重试至死信)+ 对账兜底(定时比对两侧数据,漂移自动/告警修复)= 生产级最终一致三板斧。对账常被低估:任何异步链路最终都靠它托底。
RocketMQ 事务消息四步:半消息、本地事务、commit/rollback、回查兜底。与本地消息表对比:锚点从本地 DB 挪到 broker,基础设施代劳重投回查。边界感必讲:保证可靠投递不保证可靠消费,也无隔离性——生产级闭环是"可靠投递+可靠消费+对账兜底"三板斧,对账是最后防线。注意 Kafka 事务是流处理语义不是这个模式(互链 producer deck)。
More Patterns
最大努力通知 与 Seata AT 模式
最大努力通知(尽力而为 + 校对)
- 场景:跨企业的通知(支付结果通知商户)——对方系统你控制不了
- 机制:按衰减间隔重试(1m/5m/…N 次后放弃)→ 提供查询对账接口让对方主动核对
- 与本地消息表的区别:消息表保证"我方内部一致";最大努力通知连"对方收到"都是尽力——可靠性由对方主动查询兜底
- 协议细节:通知带签名防篡改、对方响应"SUCCESS"才停、 webhook 安全(IP 白名单/超时)
Seata AT 模式(框架化的 2PC 变体)
- 思路:一阶段即提交本地事务,undo_log 记前后镜像,全局由 TC 管理
- 回滚:读 undo_log 反向补偿(自动生成反向 SQL)
- 全局锁:TC 记录"某行正被全局事务 X 修改",防止脏写(另一全局事务改同一行)——全局锁是 AT 隔离性的关键
- 对比 XA:不长期持有 DB 锁(一阶段即提交),性能高一个量级;代价是中间态可见 + 全局锁在 TC 侧
AT 的适用与风险
适用:Java 存量业务快速接入(业务零改造,注解即用)。风险:① 全局锁热点行成吞吐瓶颈(如秒杀库存);② 跨数据源类型受限;③ 复杂 SQL(批量/触发器)反向补偿可能失真。Go 无对等成熟实现——多用消息/Saga 或 dtm(支持 Saga/TCC/消息)。
dtm:Go 生态的现实选择
dtm(github.com/dtm-labs/dtm):支持 Saga/TCC/XA/二阶段消息,HTTP/gRPC 双协议——Go 微服务做"强一点的一致性"的常见选型。了解定位即可,重点是模式而非框架。
两个补充模式:最大努力通知(跨企业场景,衰减重试+对方主动查询兜底,注意签名与幂等停发);Seata AT(一阶段即提交+undo_log 反向补偿+TC 全局锁防脏写,对比 XA 不持锁性能高,风险是热点行与补偿失真)。Go 生态补充 dtm 的定位。AT 的全局锁机制是追问点。
Decision Matrix
方案选型对比表(面试可直接背)
| 方案 | 一致性 | 隔离性 | 业务侵入 | 性能 | 适用场景 |
| 2PC/XA | 强一致 | 好(锁资源) | 低(DB 层) | 差(同步阻塞) | 短事务、并发低、强一致刚需(传统金融) |
| 3PC | 理论增强 | - | - | - | 工程几乎不用(分区不安全) |
| TCC | 准强一致 | 较好(Try 预留) | 高(三接口+状态表) | 中 | 金融核心短流程(2-3 参与方) |
| Saga | 最终一致 | 无(中间态可见) | 中(补偿逻辑) | 高 | 长流程、跨多服务、参与方多 |
| 本地消息表 | 最终一致 | 无 | 低(一张表+扫描) | 高 | 异步解耦场景的事实标准 |
| 事务消息 | 最终一致 | 无 | 低(回查接口) | 高 | 同上,且已有 RocketMQ |
| 最大努力通知 | 尽力 | - | 低 | 高 | 跨企业通知(支付回调商户) |
| Seata AT | 准强一致 | 全局锁 | 最低(注解) | 中高 | Java 存量系统快速接入 |
决策树(背这棵树)
① 能否重新划边界避免跨服务?能 → 改设计;② 是否异步解耦可容忍延迟?是 → 本地消息表/事务消息(90% 场景到此为止);③ 必须同步且强一致、参与方 ≤3?→ TCC;④ 长流程多参与方 → Saga(编排式);⑤ Java 存量快速落地 → Seata AT;⑥ 跨企业 → 最大努力通知+对账。
通用兜底:对账与告警
无论选哪个方案,定时对账(T+1 或实时比对两侧数据)+ 差异告警 + 自动/人工修复都是最后防线。面试金句:"分布式事务选型决定happy path,对账决定下限——所有最终一致方案最终都要靠对账自证清白。"
选型对比表八行按一致性/隔离性/侵入/性能/场景五列。决策树是灵魂:先改边界、异步走消息(90% 场景)、同步强一致短流程 TCC、长流程 Saga、Java 存量 AT、跨企业通知。通用兜底:对账+告警——金句"选型决定 happy path,对账决定下限"。
Interview QA · 1/2
高频 QA(一):方案机制
Q1 什么是分布式事务?为什么微服务需要它?
跨库原子性
一个业务操作涉及多个服务/数据库,无法用单个本地事务保证"全成或全不成"。成因:服务拆分后每个服务私有库,RPC 调用无法纳入对方事务边界——部分失败、网络不可知、并发交错都会产生不一致。解法分刚性(2PC 系)与柔性(Saga/TCC/消息最终一致)。
Q2 2PC 的问题有哪些?怎么缓解?
阻塞单点不一致窗口
① 同步阻塞:prepare 后锁资源等决议;② 协调者单点:它挂了参与者僵持;③ commit 消息丢失部分提交。缓解:协调者日志+参与者超时询问(3PC 思路)、协调者共识化(Paxos Commit/Spanner)、超时仲裁。但互联网场景直接绕开:改用柔性事务。
Q3 TCC 的空回滚和悬挂分别是什么?
Cancel 先到Try 后到
空回滚:Try 因网络丢失未到达,Cancel 先执行——Cancel 查无 Try 记录应直接返回成功并记录"已回滚"。悬挂:Cancel 执行后阻塞的 Try 迟到——Try 检查"已回滚"标记必须拒绝执行,否则资源被白白冻结。两者加上幂等重试,统一解法是事务控制表(xid 状态机)。
Q4 Saga 和 TCC 怎么选?Saga 没有隔离性怎么办?
中间态可见
选型:TCC 有"资源预留"隔离较好但侵入大、适合短流程金融;Saga 无隔离但侵入小、适合长流程。Saga 隔离缺失的应对:① 业务语义容忍(订单"处理中"状态用户可见);② 语义锁(处理中标记阻止并发操作);③ 把不可补偿步骤放末端;④ 冷热数据隔离读。极端要求隔离 → 上升 TCC。
Q5 本地消息表和事务消息的本质区别?
锚点位置
一致性锚点不同:消息表锚在本地 DB 事务(业务+消息同库同事务,天然原子),重投自己写;事务消息锚在 broker 半消息 + 回查,基础设施代管投递与兜底。功能等价,选型看是否有 RocketMQ 类基础设施;没有就消息表(或 CDC 订阅 binlog 产生事件)。
Q6 消息重复消费怎么处理?和分布式事务什么关系?
at-least-once幂等兜底
MQ 至少一次投递语义下重复不可避免(生产者重试、消费者超时重平衡)。处理:消费侧幂等——唯一业务键去重(唯一索引/Redis setnx)、状态机校验(已处理状态直接 ack)。与分布式事务的关系:所有异步最终一致方案都以"重复必达+幂等消费"为底座——幂等是分布式事务的地基(见幂等 deck)。
六题:分布式事务定义与成因、2PC 三问题与缓解、TCC 空回滚/悬挂(必背)、Saga vs TCC 选型与隔离缺失应对、消息表 vs 事务消息(锚点位置)、重复消费与幂等地基。答题时把"幂等是所有最终一致方案的地基"这个观点带出来。
Interview QA · 2/2
高频 QA(二):场景设计
Q7 下单扣库存跨服务,怎么保证不超卖且不丢单?
预扣+异步+对账
主流组合:① 库存服务 Redis 原子预扣(Lua 判断+扣减,防超卖,见限流 deck 的 Lua 模式);② 订单服务本地事务落单 + 本地消息表发"订单已创建";③ 库存服务消费消息做 DB 正式扣减(幂等);④ 定时对账 Redis 预扣 vs DB 实扣,差异回收。同步链路只保留"预扣成功",重活全异步。
Q8 转账场景(A 扣 100、B 加 100)设计?
TCC 教科书
经典 TCC:Try——A 冻结 100、B 预登记;Confirm——A 扣减冻结、B 入账(都幂等);Cancel——A 解冻。实现要点:事务控制表防三坑;金额操作用数据库余额行锁+版本号双保险;对账兜底每日核对流水。为什么不用消息:转账要求"同买同卖"的即时对偶性,最终一致的中间态(A 扣了 B 没加)业务不可接受。
Q9 分布式事务和分布式锁什么关系?
不同问题
解决不同问题:事务解决"多个操作的原子性"(全成或全不成);锁解决"并发互斥"(同一时刻一个执行者)。联系:① TCC/AT 内部用锁保证隔离(全局锁);② 补偿/对账任务需要分布式锁防多实例重复跑(见 Redis 分布式锁 deck);③ 锁本身不提供回滚能力——不能拿锁当事务用。
Q10 你们项目里的一致性方案是怎么演进的?
演进叙事
参考叙事:初期单库事务 → 拆服务后先用"同步 RPC 链 + 失败手动补偿"(踩坑:不一致率高)→ 引入本地消息表 + 幂等消费(一致性问题收敛)→ 高并发链路上 Redis 预扣 + 异步落库 → 全链路对账系统托底。演进逻辑:从"追求强一致"到"接受最终一致 + 强兜底"。
Q11 最终一致的"窗口期"用户体验怎么处理?
中间态设计
① 状态可见:把中间态翻译成用户语言("处理中/到账中"),配查询接口;② 首屏聚合时容忍降级展示(积分可能延迟几分钟);③ 关键操作后引导"结果稍后通知"(推送/短信);④ 窗口期数据打标,对账修复后刷新。产品与工程共同设计——技术方案里带 UX 是高分信号。
Q12 如何验证分布式事务的正确性?
故障注入+对账
① 单测:mock 各参与方成功/失败/超时组合,断言补偿路径;② 故障演练:Chaos 注入参与方宕机/网络分区/消息丢失,验证最终收敛与窗口时长;③ 对账系统:数据层面的"测试永远在跑"(实时或 T+1 比对);④ 压测下验证补偿不风暴(补偿重试也要限流)。一致性正确性 = 测试 + 演练 + 对账三层。
六题场景:下单扣库存组合方案(Redis 预扣+消息+对账)、转账 TCC 教科书、事务与锁的区别(原子性 vs 互斥)、演进叙事模板(强一致→最终一致+强兜底)、窗口期 UX 处理(中间态翻译成用户语言)、正确性验证三层(单测/演练/对账)。
Related & References
相关知识点与参考
本领域相关 deck
- 幂等性设计 —— 一切最终一致方案的地基(重试/重投的护身符)
- 微服务总览 —— 数据私有与服务拆分如何制造了这个问题
- 熔断降级容错 —— 补偿重试的限流与预算约束
- 限流 —— Redis+Lua 原子操作模式的同源复用
- 配置中心 —— 对账开关/补偿参数的动态下发
收尾链接:领域内幂等(地基)、总览(问题成因)、容错(补偿约束)、限流(Lua 模式复用)、配置中心;跨领域 MySQL 事务与 MVCC(undo_log 对照)、Kafka 事务语义区分、Redis 锁(对账互斥)。参考:Richardson 微服务模式、Seata 官方文档、RocketMQ 事务消息、Gray & Lamport 论文、dtm。