📖 本篇简介:微服务 + 分库分表时代,本地事务已经管不了跨库跨服务的数据一致性,分布式事务成为中高级面试的必考点。本篇基于 MySQL 8.0 与常见微服务架构,系统讲解 CAP/BASE 理论、2PC/XA、3PC、TCC、Saga、本地消息表、最终一致性、幂等设计、分布式 ID 与全局顺序、读写一致性等核心知识点。学完后你能讲清楚"为什么微服务下很少用 XA""TCC 与 Saga 的取舍""本地消息表怎么实现最终一致""接口幂等怎么做",并能为支付/下单场景设计一套合理的分布式一致性方案。
目录
- 题目 1:什么是分布式事务,为什么需要它
- 题目 2:CAP 理论与 BASE 理论怎么理解
- 题目 3:2PC/XA 两阶段提交的原理与缺点
- 题目 4:3PC 相比 2PC 改进了什么
- 题目 5:TCC 模式是什么,三个阶段做什么
- 题目 6:Saga 模式是什么,与 TCC 区别
- 题目 7:本地消息表怎么实现最终一致
- 题目 8:事务消息(RocketMQ)怎么用
- 题目 9:幂等性是什么,为什么分布式事务必须幂等
- 题目 10:接口幂等有哪些实现方式
- 题目 11:分布式事务与本地事务的隔离级别差异
- 题目 12:读写一致性(读己之写)怎么实现
- 题目 13:MySQL XA 在分库分表下能用吗
- 题目 14:最终一致性的延迟怎么监控与治理
- 题目 15:跨服务调用失败怎么重试与补偿
- 题目 16:分布式 ID 与全局顺序性怎么保证
- 题目 17:对账系统在最终一致性中的作用
- 题目 18:TCC 空回滚、悬挂、幂等三大问题
- 题目 19:Seata 框架了解吗,AT/TCC/Saga 模式
- 题目 20:常见坑与注意事项
- 速查表:分布式事务方案速记
- 复习清单
- 练习题
题目 1:什么是分布式事务,为什么需要它
考点
- 跨库/跨服务数据一致性
- 本地事务管不了
- 典型场景
参考答案
1. 定义:一次操作涉及多个数据源(多个 MySQL 库、多个微服务、消息队列),需要这些数据源要么全部成功,要么全部回滚。
2. 为什么需要:
- 微服务架构下,订单服务、库存服务、账户服务各自数据库;
- 分库分表后一个业务操作落在多个分片;
- 本地事务只能管单库,跨库无能为力。
3. 典型场景:
- 下单:扣库存 + 创建订单 + 扣余额;
- 转账:A 扣钱 + B 加钱;
- 发红包:扣余额 + 写红包记录。
追问
- 追问:单机本地事务能管分布式吗?应对:不能,本地事务的锁/redo/undo 都是单库内。
- 追问:分布式事务一定要强一致吗?应对:大多业务最终一致即可,核心交易才要强一致。
题目 2:CAP 理论与 BASE 理论怎么理解
考点
- CAP 三选二
- AP/CP 取舍
- BASE 最终一致
参考答案
1. CAP:
- C(Consistency):一致性,所有节点同一时间看到相同数据;
- A(Availability):可用性,每个请求都能得到响应;
- P(Partition tolerance):分区容忍,网络分区时系统仍能运作。
2. 网络分区必然发生,所以 P 必须保,在 C 与 A 之间取舍:
- CP:牺牲可用性,保一致(如 Zookeeper、etcd);
- AP:牺牲强一致,保可用(如 Eureka、Nginx 无状态)。
3. BASE:
- Basically Available:基本可用;
- Soft state:软状态,允许中间状态;
- Eventually consistent:最终一致。
4. 互联网主流:BASE 思想,最终一致,不追求实时强一致。
追问
- 追问:MySQL 主从复制是 CP 还是 AP?应对:异步复制是 AP(主从可能不一致);MGR 是 CP。
- 追问:为什么互联网选 AP?应对:用户体验优先,短暂不一致可接受,强一致代价大。
题目 3:2PC/XA 两阶段提交的原理与缺点
考点
- Prepare + Commit 两阶段
- 协调者统一调度
- 阻塞、单点、性能差
参考答案
1. 两阶段:
- Prepare 阶段:协调者问所有参与者"能否提交",参与者执行事务但不提交,锁资源;
- Commit/Abort 阶段:所有参与者都 Yes,协调者发 Commit;任一 No,发 Rollback。
2. MySQL XA:数据库层面支持的 2PC,XA START/XA END/XA PREPARE/XA COMMIT。
3. 缺点:
- 同步阻塞:Prepare 阶段锁资源,性能差;
- 单点故障:协调者挂了参与者一直锁;
- 数据不一致:Commit 阶段部分参与者没收到消息;
- 生产少用。
-- MySQL XA 示例(应用层做协调者)
XA START 'xid1';
UPDATE account SET balance=balance-100 WHERE id=1;
XA END 'xid1';
XA PREPARE 'xid1';
-- 所有分支都 prepare 成功后
XA COMMIT 'xid1';
追问
- 追问:为什么互联网不用 XA?应对:阻塞、性能差、跨库锁持有久,高并发下扛不住。
- 追问:XA 适合什么场景?应对:低频、短事务、对一致性要求极高的传统企业应用。
题目 4:3PC 相比 2PC 改进了什么
考点
- 引入 CanCheck 阶段
- 超时机制
- 仍有不一致问题
参考答案
1. 3PC 三阶段:CanCheck(询问能否执行)、PreCommit(预提交)、DoCommit(提交)。
2. 改进:
- 加入超时机制,参与者等不到协调者消息可自行提交/回滚;
- 减少阻塞范围。
3. 仍有问题:网络分区时仍可能数据不一致;实现复杂,生产几乎不用。
4. 结论:3PC 只是理论改进,工业界主流还是 TCC/Saga/最终一致。
追问
- 追问:3PC 为什么没普及?应对:实现复杂、网络分区仍不一致,不如直接用业务层补偿。
- 追问:2PC 与 3PC 哪个更常用?应对:都不常用,生产用 TCC/消息最终一致。
题目 5:TCC 模式是什么,三个阶段做什么
考点
- Try 预留资源
- Confirm 确认提交
- Cancel 取消释放
- 业务侵入大
参考答案
1. TCC(Try-Confirm-Cancel):业务层面的两阶段,每个服务实现三个接口:
- Try:预留资源(如冻结金额,不实际扣);
- Confirm:确认执行(真正扣减);
- Cancel:取消(释放冻结)。
2. 特点:
- 不锁数据库资源,靠业务层冻结;
- 性能比 XA 好;
- 业务侵入大,每个接口都要写 Try/Confirm/Cancel。
3. 典型场景:支付、转账等核心交易。
转账 TCC:
Try:A 账户冻结 100(余额不变,冻结金额 +100)
Confirm:A 实际扣 100,B 加 100
Cancel:A 解冻 100
追问
- 追问:TCC 与 XA 区别?应对:XA 锁数据库行,TCC 业务层冻结,不锁行。
- 追问:Confirm/Cancel 失败怎么办?应对:重试 + 对账,保证最终成功。
题目 6:Saga 模式是什么,与 TCC 区别
考点
- 长事务拆成本地事务
- 每步失败补偿
- 适合长流程
参考答案
1. Saga:把一个长事务拆成 N 个本地事务,每个本地事务都有对应的补偿操作;任一步失败,反向执行前面的补偿。
2. 特点:
- 无锁,性能好;
- 适合长流程(跨多服务、耗时分钟级);
- 没有隔离性(中间状态对外可见)。
3. TCC vs Saga:
- TCC 有 Try 预留资源,隔离性好;
- Saga 直接做本地事务,无预留,隔离性差。
下单 Saga:
1. 创订单(补偿:取消订单)
2. 扣库存(补偿:回滚库存)
3. 扣余额(补偿:回滚余额)
任一步失败,反向补偿前面步骤
追问
- 追问:Saga 中间状态可见有什么问题?应对:用户可能看到"订单已创建但库存没扣",要业务上容忍或加状态机。
- 追问:Saga 用编排还是协同?应对:编排式(中央协调器)可控,协同式(事件驱动)解耦。
题目 7:本地消息表怎么实现最终一致
考点
- 本地事务 + 消息表
- 后台任务扫表发 MQ
- 消费端幂等
参考答案
1. 原理:
- 业务数据与消息在同一个本地事务里写入消息表;
- 后台任务扫描消息表,把消息发到 MQ;
- 消费者消费消息,执行下游操作。
2. 优点:不依赖 MQ 事务消息,本地事务保证消息不丢。
3. 流程:
-- 本地事务:同时写业务表与消息表
BEGIN;
INSERT INTO orders(...) VALUES(...); -- 业务表
INSERT INTO msg_table(dest, payload, status) -- 消息表
VALUES ('inventory', '{"order_id":1}', 'NEW');
COMMIT;
-- 后台任务扫 msg_table status=NEW,发 MQ
-- 发完更新 status=SENT
-- 消费端消费消息,扣库存,幂等处理
追问
- 追问:消息表会不会成为瓶颈?应对:消息表定期清理已完成消息;高并发可分库分表。
- 追问:消费失败怎么办?应对:重试 + 死信队列 + 人工介入。
题目 8:事务消息(RocketMQ)怎么用
考点
- RocketMQ 半消息
- 本地事务执行后提交/回滚
- 比本地消息表优雅
参考答案
1. RocketMQ 事务消息:
- 先发"半消息"(对消费者不可见);
- 执行本地事务;
- 本地事务成功,提交半消息(消费者可见);失败,回滚半消息。
2. 优点:不写消息表,MQ 保证消息与本地事务一致。
3. 对比本地消息表:更轻量,但依赖 RocketMQ。
RocketMQ 事务消息流程:
1. 生产者发半消息到 MQ
2. MQ 存半消息(消费者不可见)
3. MQ 回调生产者本地事务执行结果
4. 本地事务成功 → MQ 投递消息给消费者
5. 本地事务失败 → MQ 删除半消息
追问
- 追问:本地事务执行结果回调失败怎么办?应对:RocketMQ 定时回查生产者本地事务状态,兜底。
- 追问:Kafka 支持事务消息吗?应对:Kafka 有事务但偏生产端原子写多分区,不是这种业务事务消息。
题目 9:幂等性是什么,为什么分布式事务必须幂等
考点
- 重复请求结果一致
- 网络重试导致重复
- 所有写接口都要幂等
参考答案
1. 幂等:同一请求执行 N 次与执行 1 次效果相同。
2. 为什么必须:
- 网络超时重试、MQ 重复投递、用户重复点击;
- 分布式事务补偿也会重试;
- 不幂等就会重复扣款/重复下单。
3. 所有写接口都要考虑幂等。
追问
- 追问:GET/SELECT 天然幂等吗?应对:查询天然幂等,写接口要设计。
- 追问:扣库存接口怎么幂等?应对:用订单号做唯一约束,重复订单号直接报错或忽略。
题目 10:接口幂等有哪些实现方式
考点
- 唯一索引
- 去重表
- 状态机
- token 机制
参考答案
| 方式 | 做法 | 适用 |
|------|------|------|
| 唯一索引 | 业务唯一键建唯一索引 | 写 DB 操作 |
| 去重表 | 请求 ID 插入去重表,重复即拒绝 | 通用 |
| 状态机 | 订单状态只能单向流转 | 订单/支付 |
| token 机制 | 进入页面拿 token,提交带 token 校验 | 表单防重复提交 |
| 分布式锁 | Redis SETNX 锁请求 ID | 通用 |
-- 唯一索引幂等:订单号唯一
CREATE UNIQUE INDEX uk_order_no ON orders(order_no);
-- 重复插入报 Duplicate key,业务捕获忽略
追问
- 追问:Redis 分布式锁怎么做幂等?应对:SETNX request_id,存在则拒绝;注意锁过期与原子释放。
- 追问:状态机幂等是什么意思?应对:订单状态 PENDING→PAID→DONE,重复"支付"请求检查状态,已经 PAID 就不再扣。
题目 11:分布式事务与本地事务的隔离级别差异
考点
- 本地事务 ACID
- 分布式事务难保证隔离
- Saga/TCC 中间状态可见
参考答案
1. 本地事务:InnoDB MVCC + 锁,完整 ACID,隔离级别可选。
2. 分布式事务:
- XA:能保隔离,但性能差;
- TCC:Try 阶段冻结资源,有一定隔离;
- Saga/消息最终一致:无隔离,中间状态对外可见。
3. 业务上怎么弥补:
- 加状态机(待确认/已确认);
- 对账补偿;
- 业务上容忍短暂不一致。
追问
- 追问:Saga 中间状态可见会有什么问题?应对:用户看到"订单创建中",不能直接付款;业务设计状态机屏蔽。
- 追问:TCC 隔离性为什么好?应对:Try 冻结资源,其他事务看到的是冻结后的状态。
题目 12:读写一致性(读己之写)怎么实现
考点
- 用户写完立刻读,可能读到旧值
- 主从延迟导致
- 方案:写后读主/版本号
参考答案
1. 问题:写走主库,读走从库,主从延迟下用户刚写完读从库读不到。
2. 方案:
- 写后读主:用户写操作后的一小段时间内,读请求走主库;
- 版本号/时间戳:读请求带版本号,从库版本旧则回主库读;
- 粘性会话:同一用户的读写都路由到主库;
- 缓存:写操作后失效缓存,读走缓存。
追问
- 追问:写后读主会压垮主库吗?应对:只在写后短时间内(如 1 秒)读主,时间窗过后回从库。
- 追问:微信发朋友圈后立刻能看到怎么实现?应对:写后读主 + 本地缓存。
题目 13:MySQL XA 在分库分表下能用吗
考点
- 理论上支持
- 性能差、阻塞
- 生产少用
参考答案
1. MySQL XA:支持跨库 XA 事务,应用层做协调者。
2. 问题:
- Prepare 阶段锁各分支行,跨库锁持有久;
- 网络抖动时协调者挂了,分支长时间阻塞;
- 性能比本地事务低 50%+。
3. 生产:核心链路一般不用,用 TCC 或最终一致。
追问
- 追问:XA 事务怎么恢复?应对:
XA RECOVER查看 prepared 事务,人工XA COMMIT/ROLLBACK。 - 追问:什么场景可以用 XA?应对:低频、跨库、短事务,传统企业内部系统。
题目 14:最终一致性的延迟怎么监控与治理
考点
- 消息堆积
- 延迟监控
- 死信告警
参考答案
1. 监控指标:
- MQ 消息堆积量;
- 消息从生产到消费的延迟;
- 失败重试次数;
- 死信队列量。
2. 治理:
- 消费失败重试 3 次后进死信;
- 死信告警人工介入;
- 对账系统每天兜底。
追问
- 追问:最终一致延迟多少算正常?应对:毫秒~秒级;超过 1 分钟要告警。
- 追问:消息丢了怎么办?应对:本地消息表 + 后台扫表兜底,或对账系统发现差异补数据。
题目 15:跨服务调用失败怎么重试与补偿
考点
- 重试幂等
- 退避策略
- 死信与人工
参考答案
1. 重试原则:
- 只对幂等接口重试;
- 指数退避(1s, 2s, 4s, 8s);
- 最大重试次数(3~5 次)。
2. 补偿:
- 重试多次失败进死信;
- 死信告警 + 人工处理;
- 对账系统兜底。
3. 幂等是前提:不幂等的接口重试会造成重复操作。
追问
- 追问:同步调用失败 vs MQ 异步失败?应对:同步直接重试/降级;MQ 异步重试进死信。
- 追问:重试风暴怎么办?应对:退避 + 抖动 + 限流。
题目 16:分布式 ID 与全局顺序性怎么保证
考点
- 雪花 ID 趋势递增
- 全局严格顺序难
- 业务上不要求全局顺序
参考答案
1. 趋势递增:雪花算法保证 ID 大致按时间递增,InnoDB 插入友好。
2. 全局严格顺序:
- 单点自增能保证但性能差;
- 分布式下严格顺序代价大;
- 业务一般只要"趋势递增",不要严格顺序。
3. 严格顺序场景:如时间线消息,用 Redis INCR 或 DB 号段。
追问
- 追问:雪花 ID 时钟回拨怎么办?应对:记录上次时间,回拨等待或报错;用成熟中间件。
- 追问:UUID 为什么不行?应对:无序,InnoDB 主键性能差。
题目 17:对账系统在最终一致性中的作用
考点
- 兜底机制
- 每日 T+1 对账
- 差异自动/人工处理
参考答案
1. 作用:即使有重试、死信,仍可能有数据不一致,对账系统是最后一道防线。
2. 做法:
- 每日 T+1 跑批,对比订单库、库存库、支付库数据;
- 差异自动修复(补单/回滚);
- 无法自动修复告警人工。
3. 典型:支付对账(订单 vs 支付流水)、库存对账(订单 vs 库存扣减)。
追问
- 追问:实时对账还是 T+1?应对:核心资金实时/小时级对账;普通业务 T+1。
- 追问:对账发现差异怎么处理?应对:先保证资金安全,人工核对后补单。
题目 18:TCC 空回滚、悬挂、幂等三大问题
考点
- 空回滚:Try 没到先 Cancel
- 悬挂:Cancel 后 Try 才到
- 幂等:Confirm/Cancel 重复
参考答案
1. 空回滚:Try 请求超时没到,协调者发 Cancel;Cancel 时该分支 Try 没执行,要识别并"空回滚"(直接成功)。
2. 悬挂:空回滚后,Try 延迟到达;如果 Try 执行了,资源被冻结但没人 Confirm,悬挂住了。要识别 Cancel 已发生,拒绝 Try。
3. 幂等:Confirm/Cancel 可能重复调用,要记录状态,重复调用直接返回成功。
4. 解决:每个分支记录事务状态(TRY/CONFIRM/CANCEL),Try/Cancel/Confirm 前先查状态。
追问
- 追问:Seata TCC 框架帮你处理这些了吗?应对:是,Seata 内置防悬挂/空回滚/幂等。
- 追问:手写 TCC 容易踩坑吗?应对:容易,建议用成熟框架。
题目 19:Seata 框架了解吗,AT/TCC/Saga 模式
考点
- Seata 阿里开源
- AT 自动补偿
- TCC 业务侵入
- Saga 长事务
参考答案
1. Seata:阿里开源分布式事务框架,常用三种模式:
- AT 模式:自动拦截 SQL,反查前镜像/后镜像,自动生成补偿;业务无侵入,默认模式;
- TCC 模式:手写 Try/Confirm/Cancel;
- Saga 模式:长流程编排。
2. AT 原理:一阶段提交本地事务,记录 undo_log;二阶段根据全局事务状态决定删 undo_log 或回滚。
3. AT 局限:只支持关系型数据库,隔离性靠全局读锁,性能比本地事务低。
追问
- 追问:AT 模式有隔离性吗?应对:默认读未提交,全局读锁保证写隔离,比 XA 弱。
- 追问:Seata 适合大流量吗?应对:AT 有性能开销,核心高并发链路建议最终一致。
题目 20:常见坑与注意事项
考点
- 分布式事务高频坑
- 幂等/补偿/对账
- 业务设计
参考答案
1. 坑 1:以为 XA 能解决一切,实际性能差扛不住。
2. 坑 2:写接口不幂等,重试导致重复扣款。
3. 坑 3:TCC 手写没处理空回滚/悬挂。
4. 坑 4:MQ 消息丢了没兜底,业务数据不一致。
5. 坑 5:Saga 中间状态可见,业务没加状态机。
6. 坑 6:最终一致延迟没监控,用户投诉才发现。
7. 坑 7:对账系统缺失,差异数据长期存在。
8. 坑 8:分布式 ID 用 UUID,InnoDB 性能差。
9. 坑 9:跨服务同步调用链太长,超时雪崩。
10. 坑 10:以为微服务不用分布式事务,实际每个跨服务写都是分布式事务。
追问
- 追问:分布式事务最佳实践是什么?应对:能不用就不用(业务合并/同库),要用优先最终一致(消息+对账),核心资金才 TCC。
- 追问:为什么微服务架构下分布式事务这么多?应对:每个服务独立库,跨服务写就涉及分布式事务。
速查表:分布式事务方案速记
| 方案 | 一致性 | 性能 | 业务侵入 | 适用 |
|------|--------|------|----------|------|
| 2PC/XA | 强一致 | 差 | 无 | 传统企业低频 |
| TCC | 最终一致 | 中 | 大 | 核心支付 |
| Saga | 最终一致 | 好 | 中 | 长流程 |
| 本地消息表 | 最终一致 | 好 | 小 | 互联网主流 |
| RocketMQ 事务消息 | 最终一致 | 好 | 小 | 用 RocketMQ |
| Seata AT | 最终一致 | 中 | 无 | 业务无侵入需求 |
复习清单
| 题目 | 核心结论 | 易错点 |
|------|----------|--------|
| 1 | 跨库跨服务写要分布式事务 | 微服务下到处都是 |
| 2 | CAP 三选二,BASE 最终一致 | 互联网选 AP |
| 3 | 2PC 阻塞、单点、性能差 | 生产少用 XA |
| 4 | 3PC 理论改进,工业不用 | 记概念即可 |
| 5 | TCC Try 预留/Confirm/Cancel | 空回滚/悬挂 |
| 6 | Saga 长事务反向补偿 | 中间状态可见 |
| 7 | 本地事务+消息表+扫表发 MQ | 消息表定期清理 |
| 8 | RocketMQ 半消息+回查 | 依赖 RocketMQ |
| 9 | 所有写接口必须幂等 | 重试致重复操作 |
| 10 | 唯一索引/去重表/状态机/token | GET 天然幂等 |
| 11 | 分布式事务难保证隔离 | Saga 中间状态 |
| 12 | 写后读主解决读己之写 | 主从延迟 |
| 13 | MySQL XA 跨库但性能差 | 协调者挂分支阻塞 |
| 14 | MQ 堆积/死信/延迟监控 | 死信不告警 |
| 15 | 重试要幂等+退避 | 重试风暴 |
| 16 | 雪花趋势递增 | 时钟回拨 |
| 17 | 对账是最后兜底 | T+1 对账 |
| 18 | TCC 空回滚/悬挂/幂等 | 手写 TCC 踩坑 |
| 19 | Seata AT 无侵入但性能开销 | AT 隔离性弱 |
| 20 | 幂等/补偿/对账三大件 | 以为 XA 万能 |
练习题
1. 题目:微服务下订单服务要扣库存、写订单、扣余额,三个服务各自数据库,怎么设计?
- 参考答案:优先最终一致:订单服务本地写订单 + 本地消息表,发 MQ;库存/账户服务消费消息幂等扣减;失败重试 + 死信 + T+1 对账。
2. 题目:为什么互联网高并发场景不用 XA 两阶段提交?
- 参考答案:Prepare 阶段锁跨库资源,阻塞、性能差、协调者单点;高并发下锁持有久,吞吐骤降。
3. 题目:接口幂等有哪些常见实现方式?
- 参考答案:唯一索引、去重表、状态机、token 机制、Redis 分布式锁。
4. 题目:TCC 中"空回滚"和"悬挂"分别指什么?
- 参考答案:空回滚是 Try 没到先收到 Cancel;悬挂是空回滚后 Try 延迟到达,资源被冻结没人处理。要记录事务状态防住。
5. 题目:用户写完数据立刻读,为什么可能读到旧值?怎么解决?
- 参考答案:写走主库读走从库,主从延迟;方案:写后短时间读主、粘性会话、版本号判断。
下一篇预告:第 19 章 备份恢复与数据安全面试精讲
文章回复
0 条公开回复