← 学习门户 每日面试题 学习看板

2026-09-23

分布式事务 —— 从 2PC 阻塞到柔性事务工程落地

10 题 · 平均 2.9/5 · low 3 · medium 2.5 · high 3.33 · 第 2 轮

#1 什么是分布式事务? low system-design

题目

什么是分布式事务?

我的回答

分布式事务是和单机事务对比而来的,它本质还是事务,想要达成的目标是数据的一致性。这个一致性可能是最终一致性,也可能是强一致性,但大多数分布式事务还是以最终一致性为目标的,因为强一致性带来的整体收益相对较低。当然,不同的业务场景也会选择使用强一致性的分布式事务。分布式事务,它基本上是通过 RPC 或者是 TCC 来实现的,另外还有一个 STAY 的算法。它实现的方式 RPC 的话,就是多个事务节点确认自己能执行,然后都确认了之后,然后统一进行一个执行提交,确保不会出现某事务节点不能执行,但是其他事务节点执行完,后续处理处理困难的问题。然后另外是 TCC,TCC 的话它是先尝试,然后做对应的回滚策略,测试大家能执行,然后没问题,先尝试,然后执行,要么执行成功,要么执行失败,执行失败之后大家做补偿,把对应的事务那问题进行补偿。都是目标是,要么完成整个事务内所有内容完成,要么整个事务内所有东西都不完成。核心目的还是这个一致性。只是实现方式从原本的单机由数据库自己完成,转化为分布式的,要么是自定义业务和实现补偿,要么是一些补偿的相应的机制去相应的自己处理。这是我理解的分布式事务。

点评

评分:⭐⭐⭐☆☆
优点:- 从"单机事务对比"切入,正确指出本质仍是事务,核心目标是数据一致性 - 准确区分强一致 vs 最终一致,并说明"多数业务选最终一致"的收益权衡 - 2PC 的"prepare 全确认再 commit"流程描述清晰 - TCC 的 Try/Confirm/Cancel 补偿机制描述基本正确
不足:- 术语混淆:"RPC 实现"应为"2PC 协议";"STAY"应为"Seata" - 未提到"为什么要分布式事务"的触发场景(跨服务、跨库、跨资源) - 未把分布式事务放在 CAP/BASE 框架下讨论 - 未列举主流实现模式(2PC / TCC / Saga / 本地消息表 / 事务消息 / Max Effort) - 补偿机制的描述偏"业务自定义",未提到框架化补偿(如 Seata / Netflix Conductor) ---

完整讲解

暂无完整讲解
#2 什么是分布式事务中的两阶段提交(2PC) low system-design

题目

什么是分布式事务中的两阶段提交(2PC)

我的回答

分布系统中,两阶段提交 2PC,本质就是把单次提交的请求,一个事务内的单次提交请求分作两次去进行。先进行一次预提交,大家确认一下各自能否进行提交,能否完成提交。如果确认完状态是可以的,在这个阶段完成之后,就进行正式的提交,将所有的资源进行处理,完成一整次的提交。它的好处本质上就是防止在提交过程中,出现一些服务没有办法准备,没有准备好,导致已经提交的事务无法正常进行。而提前做了确认,确认大家预留了,这样可以确保成功率,事务的成功率,事务的稳定性。但是衍生一个问题,就是它需要产生延迟,或者说等待。因为事务提交第一个发起者要等后续的其他参与者确认之后才能够进行提交,这就是 2PC 的问题。

点评

评分:⭐⭐⭐☆☆
优点:- 正确把握 2PC 的核心:把一次提交拆成 Prepare + Commit 两阶段 - 正确描述了 Phase 1(预提交):参与者确认能否提交 - 正确描述了 Phase 2(提交):全部确认后统一提交 - 正确识别了代价:同步等待带来的延迟
不足:- 未明确两个角色:**协调者(Coordinator)+ 参与者(Participant)** - 未提到**redo log / 持久化日志**——这是 2PC 能在故障中恢复的关键 - 未提到"Prepare 之后参与者**持有资源锁直到 Commit/Rollback**"——这是阻塞的根源 - 未提到最关键的问题:**协调者宕机会导致事务永久阻塞**(阻塞才是 2PC 的核心缺陷,不是延迟) - 未从"为什么不是 1PC"角度切入(1PC 的问题是:协调者崩溃后参与者状态不明) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 2PC 协议两阶段结构的精确理解,以及能否识别其致命缺陷(阻塞)而非仅说"延迟高"。

**知识点拆解**:
1. **角色分工**:协调者(Txn Coordinator)+ 参与者(Participants),通常业务应用是协调者,数据库是参与者
2. **阶段 1 - Prepare(准备阶段)**:
- 参与者执行事务但不提交,**写 redo log 到磁盘**
- **持有资源锁**,等待协调者指令
- 返回 Prepare OK(已准备好)/ Fail
3. **阶段 2 - Commit(提交阶段)**:
- 全部 OK → 协调者发 Commit → 参与者执行提交,释放锁
- 任一 Fail → 协调者发 Rollback → 参与者回滚
4. **崩溃恢复的关键**:redo log 是 2PC 能在故障后收敛的**唯一依据**
5. **致命缺陷**:协调者崩溃在"写日志→发指令"之间 → **参与者永久阻塞**

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否能讲出 redo log 的关键作用,而不是停留在"分两阶段"
- **广度**:能否把 2PC 与 1PC / 3PC / XA 对比
- **应用**:能否识别生产环境下的阻塞问题(MySQL XA 场景)

**延伸递进面试题**:
1. **"为什么 2PC 需要写 redo log?如果不写,会出现什么问题?"**
- 要点:redo log 是"参与者本地可查询的状态记录"。没有它,协调者恢复后无法判断参与者是否准备好,只能重试 Prepare(可能重复扣款),或者回滚(可能丢失已提交的资源)
2. **"协调者宕机后,参与者持有了锁无法释放,业务上会看到什么现象?如何主动恢复?"**
- 要点:锁持有 → 其他事务阻塞 → 连接池耗尽。恢复方案:手动干预(清理 redo log 强制 Rollback)、超时回滚机制、协调者故障转移
3. **"MySQL 的 XA 事务本质就是 2PC,为什么生产上不建议用?"**
- 要点:XA 的阻塞在跨库场景中尤其严重(协调者=业务应用),且 MySQL XA 只解决"单个 DB 内的多表原子性",跨 DB 仍需业务层协调

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,2PC 是分布式事务里最经典的强一致协议,核心思想是**把一次提交拆成两个阶段**,用协议来弥补没有单一事务边界的问题。
>
> 它有两个角色:协调者和参与者。协调者通常是业务应用,参与者是各个数据库或微服务。
>
> **第一阶段是 Prepare**。协调者向所有参与者发"你能提交吗"的询问。每个参与者收到后,**先在本地执行事务但不提交**,同时把这次操作的日志写到 redo log 并落盘——这一步是关键,因为 redo log 是参与者"我能恢复的判断依据"。然后参与者**持有资源锁**等待协调者下一步指令,返回 Prepare OK 或者 Fail。
>
> **第二阶段是 Commit**。如果所有参与者都返回 OK,协调者发 Commit,参与者真正提交并释放锁;如果有一个返回 Fail,协调者发 Rollback,所有参与者回滚。
>
> 这里有个关键点:**redo log 让 2PC 具备崩溃恢复能力**。假设协调者在发出 Commit 之前宕机,参与者通过查询 redo log 就能知道"我当时是准备 OK 还是 Fail",恢复后可以自行决定是否提交;如果没有 redo log,参与者只能被动等待或者超时回滚,就可能造成状态丢失。
>
> 但 2PC 最大的问题不是延迟,而是**阻塞**。想象一下:协调者在"写完日志→发 Commit"这个窗口宕机了,所有参与者都持有资源锁,谁也无法推进,事务就卡住了。业务上会看到大量连接被占用、其他事务无法执行。这是 CP 系统的必然代价——为了保证一致性,牺牲可用性。
>
> 3PC 想通过引入"预提交"阶段来缓解阻塞,但工程实践依然没有替代 2PC,因为网络分区依然存在。所以现在主流方案要么用 XA(MySQL 内置),要么转向柔性事务——TCC、Saga、消息驱动的最终一致方案,用"最终一致"换"可用性"。

#3 什么是柔性事务? low system-design

题目

什么是柔性事务?

我的回答

柔性事物本质上就是针对所谓的刚性事物来定义的一个名称。其实应该是针对于强一致性跟弱一致性的问题,最终一致性,而并不是弱一致性。柔性事物就是能允许中间存在回滚、测试、重试,然后它有一定中间态,然后在最后能保证一致,而不是一次性的全量的保持一致。

点评

评分:⭐⭐⭐☆☆
优点:- 正确从"刚性事务"对比切入,抓到了核心哲学差异 - 准确指出柔性事务 = 最终一致,**而非弱一致**(这是常见误区) - 正确理解"允许中间态、最后收敛"的本质
不足:- 未提到理论根基:**BASE 理论**(Basically Available, Soft state, Eventually consistent) - 未列举具体的柔性事务实现模式(TCC / Saga / 本地消息表 / 事务消息 / 最大努力通知) - 未列举刚性事务对应物(2PC / XA)形成对照 - 未说清"柔性"的代价是什么、收益是什么(可用性、吞吐换一致性) - 术语小瑕疵:"回滚、测试、重试"应为"**补偿、重试、最终收敛**" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对"强一致 vs 最终一致"哲学差异的理解,以及能否把柔性事务放到 BASE 理论框架下认知。

**知识点拆解**:
1. **柔性事务的定义**:放宽 ACID 中的一致性(C)和隔离性(I),追求最终一致,而非每一步都强一致
2. **理论根基**:BASE 理论是对 ACID 的哲学反对——**Basically Available**(基本可用)、**Soft state**(软状态)、**Eventually consistent**(最终一致)
3. **柔性 vs 刚性的本质差异**:
- 刚性:同步、强一致、阻塞、CP
- 柔性:异步/补偿、最终一致、非阻塞、AP
4. **工程权衡**:柔性事务用"短暂不一致"换"高可用 + 高吞吐 + 低延迟"
5. **实现模式谱系**:
- 高侵入高一致:TCC
- 中侵入中一致:Seata AT
- 低侵入最终一致:Saga / 本地消息表 / 事务消息 / 最大努力通知

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否理解柔性事务是"哲学选择"而非"技术降级"
- **广度**:能否把 BASE 理论、CAP 理论、ACID 三个理论串起来
- **应用**:能否按业务场景推荐合适的柔性事务模式

**延伸递进面试题**:
1. **"BASE 中的 'Soft state' 是什么意思?和传统数据库的 '强状态' 有什么区别?"**
- 要点:Soft state 指状态可以在一段时间内"不精确但可收敛",比如缓存的短暂不一致;强状态则要求每次读都看到最新写入
2. **"最大努力通知为什么算柔性事务?它的一致性保证到底有多'弱'?"**
- 要点:最大努力通知只保证"尽力通知 N 次",不保证业务成功,是柔性事务中最弱的一类,适合"通知"类业务(如支付成功通知)
3. **"如果老板问'为什么我们不用 2PC 保证强一致,非要用柔性事务?'你会怎么回答?"**
- 要点:2PC 阻塞严重,大促时协调者宕机=全站不可用;业务实际可容忍秒级不一致;柔性事务吞吐量比 2PC 高 10-100 倍

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,柔性事务是相对刚性事务而言的一个概念。刚性事务追求强一致性,典型代表是 2PC、XA,每一步都必须同步确认、全部成功才算成功;柔性事务则放宽了对 ACID 的要求——尤其是 C(一致性)和 I(隔离性),允许中间存在短暂不一致状态,最终通过补偿、重试、对账等机制收敛到一致。
>
> 柔性事务的理论根基是 **BASE 理论**——Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致)。它本质上是对 CAP 的一种选择:**在不可避免的 P(网络分区)前提下,选 AP 而不是 CP**,也就是用可用性换一致性。这里的"一致"是"最终一致",而不是弱一致——弱一致是完全放弃一致,最终一致是"晚点一定到达"。
>
> 柔性事务允许中间态:比如订单创建后,库存先扣减,支付再异步确认,中间可能存在"订单已创建但库存未扣"的短暂不一致,但通过补偿、重试,最终一定能收敛到"订单取消+库存恢复"或"订单成功+库存已扣"两种终态之一。
>
> 常见的柔性事务实现模式我一般分三类:
> - **高侵入高一致**:TCC,业务实现 Try/Confirm/Cancel 三个方法
> - **中侵入中一致**:Seata AT,框架自动代理,业务代码改动小
> - **低侵入最终一致**:Saga 长链路编排、本地消息表(本地事务+扫表发消息)、RocketMQ 事务消息、最大努力通知
>
> 业务场景映射上,我的判断原则是:核心账务、资金流水 → 刚性事务;订单主链路(扣库存+扣款)→ TCC 或 Seata AT;通知、积分、风控 → 柔性事务。核心思想是**分层治理**:主链路保证强一致,副链路用柔性事务换性能。
>
> 工程上最重要的经验是:柔性事务不是"技术降级",而是"哲学选择"。它用"短暂的中间态"换来了系统的高可用和高吞吐,是分布式系统能扛住大促流量的基础。但代价是需要业务额外做补偿、对账、幂等、防悬挂等兜底工作——这些永远无法被框架完全自动化。

#4 常见的分布式事务有哪些? medium system-design

题目

常见的分布式事务有哪些?

我的回答

常见的分工数主要方案,也就是强一致性跟弱一致性两个方向。强一致性像 2PC、3PC,一次性成功就成功,不成功大家就一起回滚,没有什么中间态。然后另外还有就是 TCC,尝试尝试去提交,然后确认或者取消,做事务的补偿机制,而不是做事务的占留。它是在先把事务执行,然后根据如果出现异常了,就进行回滚的补偿机制来实现的。然后还有一个 STAY。它里边自带几种模式。

点评

评分:⭐⭐☆☆☆
优点:- 正确按"强一致 vs 弱一致"两个方向拆分 - 正确列举了 2PC、3PC 作为强一致代表 - 正确理解 TCC 是"先执行再补偿",不是"先占留再补偿"
不足:- **遗漏柔性事务的具体模式**:未提 Saga、本地消息表、事务消息(RocketMQ)、最大努力通知 - **未列举 Seata 的 4 种模式**:AT、TCC、Saga、XA(提到"STAY 有几种模式"但未展开) - **术语连续错误**:"STAY" 应为 **"Seata"**(连续 3 题都是这个错) - **回答太单薄**:medium 难度应该至少列举 4-5 个具体模式并简短说明,回答只点到 3 个名字 - 对 TCC 的说明偏少,没讲三个方法各自的语义(Try 冻结资源、Confirm 提交、Cancel 补偿) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对分布式事务模式的**广度认知 + 分类能力 + 场景选型**。相比 2026-09-23 那次,本次的分类框架和业务场景化明显提升。

**知识点拆解**:

1. **按一致性强度分类**(核心维度)
- **强一致**:2PC / 3PC / XA / Seata AT
- **准强一致**:TCC(业务补偿保证终态一致)
- **最终一致**:Saga / 本地消息表 / 事务消息 / 最大努力通知

2. **各模式特点对照表**

| 模式 | 一致性 | 侵入性 | 性能 | 阻塞 | 典型场景 |
|------|--------|--------|------|------|---------|
| 2PC | 强 | 低 | 低 | **会阻塞** | 早期银行 |
| 3PC | 强 | 低 | 低 | 可能阻塞 | 学术为主 |
| XA | 强 | 低 | 低 | 会阻塞 | MySQL 跨库 |
| Seata AT | 强 | 低 | 中 | 不阻塞 | 订单+库存 |
| TCC | 强 | **高** | 中高 | 不阻塞 | 核心金融 |
| Saga | 最终 | 中 | 高 | 不阻塞 | 长链路编排 |
| 本地消息表 | 最终 | 中 | 高 | 不阻塞 | 上下游解耦 |
| 事务消息 | 最终 | 低 | 高 | 不阻塞 | 支付通知 |
| 最大努力通知 | 最终 | 低 | 高 | 不阻塞 | 第三方通知 |

3. **Seata 的 4 种模式**
- **AT**:自动代理数据源,业务代码改动小;基于 undo log 实现回滚
- **TCC**:业务显式实现三个方法,可控性强
- **Saga**:长链路状态机编排,适合多步业务流程
- **XA**:兼容 JTA,本质是 2PC

4. **TCC 三方法语义**(本次回答遗漏的关键点)
- **Try**:冻结资源(如扣冻结余额,不实际扣款),预留资源
- **Confirm**:确认提交(如真的扣款),Try 成功后才执行
- **Cancel**:取消补偿(如释放冻结),Try 失败或下游 Cancel

5. **本地消息表机制**
```
本地事务:insert order + insert message_table ← 同一本地事务
定时扫表:扫 status=INIT 的消息 → 发 MQ → 更新 status=SENT
消费端:幂等消费,失败重试
```

6. **事务消息(RocketMQ)机制**
```
业务发 half message → MQ 暂存 → 执行本地事务 → 提交/回查
本地事务成功 → commit half message → 正常投递
本地事务失败 → rollback half message → 不投递
不确定 → 回查业务方
```

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲出每个模式的**核心机制**(如 Seata AT 基于 undo log、TCC 三方法语义)
- **广度**:能否系统性地列举并分类,覆盖强/最终两派 + Seata 的 4 模式
- **应用**:能否结合业务场景(金融 vs 订单 vs 通知)推荐合适模式

**延伸递进面试题**:
1. **"Seata AT 和 TCC 都号称'强一致',它们的区别是什么?"**
- AT 基于 undo log 自动回滚,对业务透明但需要全局锁/间隙锁;TCC 是业务显式补偿,侵入高但锁粒度更细
2. **"本地消息表和 RocketMQ 事务消息,本质区别是什么?"**
- 本地消息表靠"业务方自建扫表",事务消息靠"MQ 内置回查";前者依赖业务方维护,后者依赖 MQ 实现
3. **"Saga 的'补偿事务'如何保证本身不出错?"**
- 补偿事务本身也必须是幂等的;通过状态机设计 + 每步可查询状态避免重复补偿

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,分布式事务的模式我一般按一致性强度分三类。
>
> **强一致类**主要有:2PC、3PC、XA、Seata AT。2PC 是经典的 Prepare+Commit 两阶段协议,XA 是 MySQL 内置的实现。3PC 是 2PC 的改进版,加了一个预提交阶段来缓解阻塞,但工程实践中没有替代 2PC。Seata AT 是"自动补偿"模式,框架代理数据源,业务代码基本不用改。
>
> **准强一致类**主要是 TCC。业务需要实现三个方法:Try 冻结资源(比如扣冻结余额,不实际扣款)、Confirm 提交(真的扣款)、Cancel 补偿(释放冻结)。TCC 侵入性最高,但可控性和审计性也最好,适合核心账务、跨支付等强一致场景。
>
> **最终一致类**主要有:Saga、本地消息表、事务消息、最大努力通知。
> - Saga 是长链路状态机编排,每个活动有对应补偿,适合多步业务流程
> - 本地消息表是"本地事务+消息表+定时扫表",业务方自建扫表逻辑,上下游解耦
> - RocketMQ 事务消息用 half message + 回查机制,把"扫表"内置到 MQ 里,业务方代码更简洁
> - 最大努力通知是最弱的一类,只保证"尽力通知 N 次",适合支付成功通知、物流同步
>
> 另外 Seata 框架本身有 4 种模式:AT、TCC、Saga、XA,分别覆盖强一致到最终一致的不同场景。
>
> 落到业务选型上,我的判断原则是:**核心账务 → TCC/XA;订单主链路 → Seata AT 或 TCC;通知/积分/风控 → 本地消息表或事务消息**。主链路保证强一致,副链路用最终一致换性能,这是分层治理的核心思想。
>
> 选型不是看技术先进性,而是看业务对一致性的容忍度——这也是为什么柔性事务在大厂业务里越来越主流。

---

#5 Seata 的 AT 模式的实现原理 medium system-design

题目

Seata 的 AT 模式的实现原理

我的回答

对于 seata 我了解不多,业务中使用分布式事务本身就比较少,尽可能的不去把事务做到不同的服务上。但是可能一些特殊场景会考虑到使用分布式事务,也会使用最终一致性的,很少。当前的业务,我接触的业务很少使用到强一致性的分布式事务。我印象中,AT 模式是强一致的事务,它是基于数据库的 undo log 去实现的,应该。它应该也是类 RPC 的模式,具体的我就没有了解了。

点评

评分:⭐⭐☆☆☆
优点:- **诚实**:坦诚说明业务接触少,态度可取 - **架构经验**:业务上倾向"减少分布式事务"是好意识(分布式事务成本高) - **基本概念正确**:AT 是强一致、基于 undo log
不足:- 未提到三个角色:**TC(Transaction Coordinator)+ TM(Transaction Manager)+ RM(Resource Manager)** - 未描述 AT 模式的核心流程(一阶段/二阶段) - 未提到**全局锁(Global Lock)**——这是 AT 模式解决数据一致性的关键 - 未提到**代理数据源**机制(Seata 拦截 SQL) - 未提到**前镜像/后镜像(before image / after image)** - 未提到**分支事务日志(branch_log)**——RM 与 TC 的交互载体 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Seata AT 模式"自动化分布式事务"机制的精确理解——三角色、两阶段、全局锁、undo log。

**知识点拆解**:

1. **三角色架构**
- **TC(Transaction Coordinator)**:Seata Server,协调全局事务,维护全局事务状态
- **TM(Transaction Manager)**:业务应用内的代理,负责开启全局事务、提交或回滚
- **RM(Resource Manager)**:代理 DataSource,管理分支事务,与 TC 通信

2. **AT 模式两阶段流程**

```
一阶段(本地事务阶段):
业务 SQL 执行:
1. 拦截 SQL,读取业务数据前镜像(before image)
2. 执行业务 SQL
3. 读取业务数据后镜像(after image)
4. 在同一本地事务内:业务 SQL + 写 undo_log(存 before/after image)
5. RM 向 TC 申请全局锁(行级锁)
6. 本地事务提交(业务数据和 undo_log 一起落盘)

二阶段(全局事务阶段):
成功路径:TC 让各 RM 异步删除 undo_log
失败路径:TC 让各 RM 读 undo_log,反向生成回滚 SQL,本地事务回滚
```

3. **全局锁的作用**
- **目的**:防止一阶段提交后、二阶段提交前,其他事务修改了同一行数据
- **实现**:基于行级锁,TC 记录 lock_record 表
- **代价**:全局锁存在期间,同一行不能有其他事务修改,影响并发性能

4. **undo_log 的作用**
- Seata 框架自动创建表(业务需手动建表,DDL 见官方文档)
- 存 before/after image(JSON 格式)
- 回滚时框架自动读取 undo_log,生成"反向 SQL"(如 `UPDATE t SET col = 'old_value' WHERE id = 1`)

5. **代理数据源的无感机制**
- 业务配置 SeataDataSourceProxy 替换原 DataSource
- 所有 SQL 都被 JdbcProxy 拦截
- 业务代码完全无感知

6. **与 2PC / TCC / XA 的对比**

| 维度 | 2PC/XA | Seata AT | TCC |
|------|--------|----------|-----|
| 侵入性 | 低 | **低** | 高 |
| 一致性 | 强 | 强 | 强 |
| 阻塞 | 会 | **不阻塞**(一阶段后释放本地锁) | 不阻塞 |
| 锁机制 | 行锁(数据库级) | **全局锁(Seata 级)** | 业务锁 |
| 回滚方式 | 数据库 redo log | **undo_log(业务层)** | 业务代码 |

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否能讲出全局锁、undo_log、三角色的具体作用
- **广度**:能否对比 AT vs TCC vs XA 的锁机制差异
- **应用**:能否识别 AT 模式在高并发场景下的瓶颈(全局锁)

**延伸递进面试题**:
1. **"Seata AT 的全局锁,会不会成为性能瓶颈?怎么优化?"**
- 要点:全局锁按行加锁,一阶段提交后立即释放本地锁但保留全局锁到二阶段结束;高并发同一行会串行化。优化:缩短事务、批量提交、换 TCC
2. **"undo_log 表会无限增长吗?怎么清理?"**
- 要点:二阶段成功后异步删除;TC 有定时清理任务;如果 TC 故障,undo_log 会滞留,需要人工清理或恢复 TC 后自动清理
3. **"Seata AT 支持非 MySQL 数据库吗?"**
- 要点:支持 PostgreSQL、Oracle、SQL Server 等,但需要各自方言的 undo_log 生成逻辑;MyCAT 等分库分表中间件有特殊适配

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,Seata AT 是 Apache Seata 提供的"自动补偿"模式,核心思想是**让业务代码无感知地享受分布式事务**,靠框架代理数据源和 undo_log 来实现。
>
> AT 模式有三个角色:
> - **TC**(Transaction Coordinator):Seata Server,负责协调全局事务
> - **TM**(Transaction Manager):业务应用里的代理,发起全局事务、决定提交或回滚
> - **RM**(Resource Manager):代理 DataSource,管理分支事务
>
> **一阶段**是本地事务阶段。业务 SQL 执行时,RM 会拦截这条 SQL,先读取业务数据的"前镜像",然后执行 SQL,再读取"后镜像",最后在同一本地事务里把 undo_log(存 before/after image)和业务数据一起写盘。RM 同时向 TC 申请一个**全局锁**——这是行级锁,是 AT 模式保证一致性的关键。本地事务提交后,本地锁释放,但全局锁仍然保留,直到二阶段结束。
>
> **二阶段**是全局事务阶段。如果业务全部成功,TC 让各 RM **异步删除** undo_log,事务结束;如果业务失败,TC 让各 RM 读取 undo_log,**反向生成回滚 SQL**(比如把数据改回 before image),用本地事务回滚。
>
> 这里有个关键点:**全局锁是 AT 模式的灵魂**。它防止"一阶段提交后、二阶段提交前"其他事务修改同一行数据。代价是高并发场景下,同一行会被串行化,这是 AT 模式的性能瓶颈。如果业务并发很高,可以考虑 TCC——TCC 的锁是业务层自己实现的,粒度更细。
>
> 和 2PC/XA 相比,AT 模式的好处是**一阶段提交后就释放本地锁**,参与者不阻塞;和 TCC 相比,AT 的好处是**业务代码无感**,框架自动处理。所以 AT 模式适合"订单+库存+支付"这种典型微服务场景,业务改动最小。
>
> 不过要提醒一下,生产用 AT 模式前需要做好几件事:建好 undo_log 表、配好 Seata Server 高可用、监控全局锁占用情况。否则全局锁堆积或 TC 故障会导致事务长期悬挂。

---

#6 TCC 中,Confirm 或者 Cancel 失败了怎么办? medium system-design

题目

TCC 中,Confirm 或者 Cancel 失败了怎么办?

我的回答

PEC 的提交或者回滚如果失败了,主流程第一个应该是记录,把这个事情整个记录下来,然后进行尝试,尝试重试,如果还是不行的话,就进行人工告警,消息告警,告警到人工去进行记录处理、接入处理。

点评

评分:⭐⭐⭐☆☆
优点:- 正确把握了核心思路:**记录 → 重试 → 人工兜底** - 流程方向正确:先记录失败现场,再自动重试,最后告警人工介入
不足:- 未提到 **幂等性(idempotency)**——这是重试的**前提条件**,否则重试会重复扣款/重复发货 - 未提到 **超时机制**——重试次数和间隔必须有上限,不能无限重试 - 未提到 **空回滚(empty rollback)** 防御——Cancel 到达但 Try 从未执行 - 未提到 **悬挂(dangling)** 防御——Confirm 先到达被拒绝,随后 Try 又到达 - 未提到 **对账/巡检** 机制——人工兜底前的最后一道自动化防线 - 回答偏单薄:medium 难度应该覆盖"自动恢复 + 人工兜底"两条链路,且点出 TCC 的三大坑 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 TCC 框架"失败处理机制"的完整认知——不止"重试+人工",还要覆盖幂等、空回滚、悬挂三大坑。

**知识点拆解**:

1. **失败处理的完整链路(6 步)**

```
1. 失败记录:写失败日志 + 更新事务状态表(FAILED)
2. 幂等重试:定时任务扫 FAILED → 基于业务单号幂等执行
3. 超时兜底:重试 N 次仍失败 → 标记 NEED_MANUAL
4. 告警通知:短信/钉钉/飞书通知值班
5. 对账巡检:日终对账系统兜底,发现不一致生成差错账
6. 人工介入:工程师查失败现场,手工补偿
```

2. **幂等性是重试的前提(最关键)**
- Confirm/Cancel 重试时会**多次执行**,如果方法本身不幂等,就会重复扣款、重复发货
- 实现方式:基于业务单号(如 `txn_id`)建唯一键,重复请求直接返回上次结果
- **口诀**:Confirm 幂等 + Cancel 幂等 + Try 幂等,三者缺一不可

3. **TCC 三大坑(必须同时处理)**

| 坑 | 触发条件 | 表现 | 解决 |
|---|---------|------|------|
| **空回滚** | Try 未执行,Cancel 到达 | Cancel 操作不存在的资源 | Try 标记表检查,未 Try 则 Cancel 直接成功 |
| **幂等** | Confirm/Cancel 重试 | 重复扣款/发货 | 三方法都基于业务单号做幂等 |
| **悬挂** | Confirm 先到被拒,Try 后到 | 资源被 Try 占用但事务已判失败 | Try 前检查事务状态,已确认/已取消则拒 Try |

4. **超时回查机制**
- Seata TCC 框架会**定期回查**分支事务状态
- 如果分支事务长时间未 Confirm,框架会主动询问:"你成功了吗?"
- 业务方需要实现"状态查询接口"(如 `queryConfirmStatus(txn_id)`)

5. **对账系统的兜底作用**
- TCC 不是 100% 可靠的,极端情况下(如 TC 故障 + 业务方宕机)仍可能不一致
- 对账系统是**最后一道防线**:日终/小时级扫描所有事务状态,发现不一致生成差错账
- 大厂金融系统标配:TCC + 对账 + 人工兜底

6. **工程最佳实践**
- 使用 Seata TCC 框架(自动处理空回滚/幂等/悬挂)
- 自建 TCC 必须处理三大坑 + 超时回查 + 对账
- 失败重试次数建议 3-5 次,间隔指数退避(1s → 2s → 4s)
- 告警要分级:普通失败→钉钉,超时失败→电话/短信

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否理解"重试"必须配合"幂等"才能安全
- **广度**:能否列出 TCC 三大坑并给出解决方案
- **应用**:能否设计完整的失败处理链路(不止重试+人工)

**延伸递进面试题**:
1. **"TCC 的 Cancel 失败了,但是 Try 已经成功了,这时候业务数据处于什么状态?怎么恢复?"**
- 要点:资源已被 Try 冻结但未 Cancel,状态悬挂。恢复:定时扫表查所有 Try 状态超过 T 的事务,强制 Cancel 并记录差错账
2. **"Seata TCC 框架的'超时回查'是怎么工作的?业务方需要做什么?"**
- 要点:TC 定时扫描未完成的分支事务,调用业务方的 `queryConfirmStatus` 接口;业务方要实现状态查询接口
3. **"如果 TCC 框架都故障了,怎么兜底?"**
- 要点:对账系统是终极兜底。日终对账对比所有事务的最终状态,发现不一致生成差错账,人工介入

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,TCC 的 Confirm 或 Cancel 失败处理,我一般按"自动恢复 + 人工兜底"两条链路设计。
>
> **自动恢复链路**分三步:
> 1. **失败记录**:把失败现场写到事务状态表,状态标记为 FAILED,记录失败原因、尝试次数
> 2. **幂等重试**:定时任务扫描 FAILED 状态的事务,调用 Confirm/Cancel 重试。但这里有个关键点——**Confirm 和 Cancel 必须幂等**,因为重试会多次执行,如果不幂等就会重复扣款、重复发货。实现方式是基于业务单号建唯一键,重复请求直接返回上次结果
> 3. **超时兜底**:重试 3-5 次仍失败,标记为 NEED_MANUAL,进入告警链路
>
> **人工兜底链路**分三步:
> 1. **告警通知**:通过短信/钉钉/飞书通知值班工程师
> 2. **对账巡检**:日终对账系统扫描所有事务状态,发现不一致生成差错账,这是最后一道防线
> 3. **人工介入**:工程师查失败日志、事务状态表、资源状态,手工补偿
>
> 这里还有个关键——TCC 有三大坑必须同时处理:
> - **空回滚**:Try 因网络问题从未执行,但 Cancel 因协调者崩溃恢复而到达。解决:在 Try 标记表检查,未 Try 则 Cancel 直接返回成功
> - **幂等**:Confirm/Cancel 重试。解决:三个方法都必须基于业务单号做幂等
> - **悬挂**:Confirm 先到达被拒绝(无 Try 记录),随后 Try 才到,资源被 Try 占用但事务已判失败。解决:在 Try 前检查事务状态表,已确认/已取消则拒绝 Try
>
> Seata TCC 框架已经自动处理了这三大坑,但业务自建 TCC 一定要自己处理。另外,Seata 还有"超时回查"机制——TC 会定期回查分支事务状态,业务方需要实现一个状态查询接口,告诉 TC "我到底成功了没有"。
>
> 工程最佳实践:Confirm/Cancel 失败重试建议 3-5 次,间隔指数退避(1s → 2s → 4s);告警要分级,普通失败走钉钉,超时失败走电话/短信;**对账系统是终极兜底**,TCC 不是 100% 可靠,极端故障下靠对账发现不一致。
>
> 总结一句话:**重试的前提是幂等,幂等的前提是三大坑都处理好,三大坑处理好的前提是业务方愿意投入**。这也是为什么核心业务往往用 Seata TCC 而不是自建 TCC。

#7 如何基于本地消息表实现分布式事务? medium system-design

题目

如何基于本地消息表实现分布式事务?

我的回答

所谓本地消息表,其实就是定义一个相当于工作链的东西,根据不同的状态去做不同处理。每个状态创建一个对应的本地消息,或者是数据库的记录。然后这时候根据这个消息的状态、消息的记录,在不同的服务去查到这个记录之后,执行自己的事。可能是转到 MQ,或者是怎么样。然后做,根据最后的事务状态做消息状态去,如果成功了就无所谓了,整个事务正常。如果失败了,可能就走定时任务去做处理,或者是失败了发送 MQ 消息,然后对应的有监听的服务去对应的做补偿机制或者是处理。

点评

评分:⭐⭐⭐☆☆
优点:- 正确理解本地消息表本质是"状态驱动的工作链" - 正确识别状态转换决定不同处理逻辑 - 正确提到"定时任务"+"MQ 监听"两条补偿链路
不足:- **未明确核心机制**:本地事务 = 业务 SQL + 写消息表(**同一事务**)——这是本地消息表的灵魂 - **未明确"定时扫表"是关键组件**(提到定时任务但没说扫哪张表) - **未提到状态机**:INIT → SENT → DELIVERED → ACKED 的完整状态流转 - **未提到消费端幂等**(防止重复消费) - **未提到消息去重**(防止重复投递) - 回答偏笼统,没给出具体的执行流程 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对本地消息表"用本地事务换分布式事务"的精确理解——核心是"业务 SQL + 写消息表 同一事务"。

**知识点拆解**:

1. **本地消息表的本质(最关键)**
- **本质**:把"分布式事务"拆解为"本地事务 + 异步消息"
- **核心思想**:业务数据和消息在**同一本地事务**里写入,保证"业务成功 = 消息存在"
- **为什么能解决分布式事务**:
- 业务成功 → 消息一定存在(同一事务,要么都成功要么都失败)
- 消息存在 → 一定能被扫表投递(定时任务保证)
- 消息投递 → 消费端一定能处理(消费端幂等保证)

2. **完整状态机**
```
INIT (本地事务已提交,消息待发送)
↓ 定时扫表 + MQ 投递
SENT (消息已发送,等待消费)
↓ 消费端 ACK
DELIVERED (消费端已确认)
↓ 业务处理完成
ACKED (业务已完成)
```
每个状态转换都是**幂等**的(重复执行结果一致)

3. **核心组件**
- **消息表**:存业务消息(msg_id、biz_id、payload、status、retry_count、next_retry_time)
- **扫表任务**:定时扫描 status=INIT 的消息,发 MQ,更新状态
- **消费端**:消费 MQ 消息,幂等处理业务,更新消息状态

4. **可靠性保证(三大目标)**
- **不丢失**:本地事务保证"业务成功 → 消息必发"
- **不重复**:消息表主键去重 + 消费端幂等
- **不丢单**:定时扫表重试 + 消费端重试

5. **与 RocketMQ 事务消息对比**
| 维度 | 本地消息表 | RocketMQ 事务消息 |
|------|-----------|------------------|
| 扫表逻辑 | **业务方自建** | MQ 内置回查 |
| 业务侵入 | 中 | 低 |
| 可靠性 | 依赖业务方 | 依赖 MQ |
| 部署复杂度 | 低 | 中 |

6. **工程实现要点**
- 消息表字段:`msg_id`(唯一键)、`status`、`retry_count`、`next_retry_time`、`create_time`
- 扫表 SQL:`SELECT * FROM msg_table WHERE status = 'INIT' AND next_retry_time < NOW() LIMIT 100 FOR UPDATE SKIP LOCKED`
- 消费端幂等:基于 `msg_id` 建唯一键,重复消息直接返回成功
- 死信队列:超过最大重试次数的消息转入死信队列,人工处理

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否理解"本地事务 + 消息表"是分布式事务的核心思路
- **广度**:能否对比本地消息表 vs RocketMQ 事务消息
- **应用**:能否设计完整状态机和扫表逻辑

**延伸递进面试题**:
1. **"如果定时扫表任务挂了,消息怎么办?"**
- 要点:消息会一直停留在 INIT 状态,不会被投递。恢复方案:扫表任务集群化(多实例)+ 监控告警 + 人工触发兜底
2. **"扫表任务多实例并发运行时,怎么避免重复投递?"**
- 要点:用 `FOR UPDATE SKIP LOCKED` 锁住消息行,其他实例跳过;或加分布式锁
3. **"本地消息表怎么和 RocketMQ 事务消息选型?"**
- 要点:业务方运维能力强、想自建 → 本地消息表;想用成熟 MQ、代码简洁 → RocketMQ 事务消息

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,本地消息表的核心思想是**用"本地事务 + 异步消息"换"分布式事务"**,让业务方在本地事务里就把消息写进去,保证"业务成功 = 消息存在"。
>
> **完整流程分三步**:
>
> **第一步是本地事务阶段**。业务代码在一个本地事务里同时做两件事:执行业务 SQL(比如 `INSERT order`)和写消息表(`INSERT message_table`)。这里的关键是**两条 SQL 在同一个本地事务里**——要么都成功,要么都失败。这样保证了"业务成功 = 消息存在",不会出现"业务成功了但消息没写"的丢单问题。消息表里要存:消息 ID(唯一键)、业务 ID、消息内容、状态(INIT/SENT/DELIVERED/ACKED)、重试次数、下次重试时间。
>
> **第二步是消息投递阶段**。有一个定时任务在后台扫表,扫 `status = 'INIT' AND next_retry_time < NOW()` 的消息,通过 `FOR UPDATE SKIP LOCKED` 加行锁避免多实例重复扫到,然后把这些消息投递到 MQ,更新消息状态为 `SENT`。如果投递失败,更新重试次数和下次重试时间,等下一轮再扫。
>
> **第三步是消费确认阶段**。消费端消费 MQ 消息,必须做**幂等处理**——基于消息 ID 建唯一键,重复消息直接返回成功。业务处理成功后,回写消息表状态为 `DELIVERED` 或 `ACKED`。如果消费失败,进入消费端重试;超过最大重试次数转入死信队列,人工处理。
>
> 这里有个关键点:**本地消息表的可靠性靠三件事保证**——本地事务保证"不丢"、消息表主键 + 消费端幂等保证"不重复"、定时扫表 + 消费端重试保证"不丢单"。
>
> 和 RocketMQ 事务消息相比,本地消息表的扫表逻辑是业务方自己建的,而 RocketMQ 内置了 half message + 回查机制,业务方代码更简洁。选型看业务方运维能力:想自建可控 → 本地消息表;想用成熟 MQ → RocketMQ 事务消息。
>
> 工程落地时还有几个细节要注意:扫表任务要集群化(多实例),监控要覆盖消息积压、状态异常;死信队列要有人工处理流程;消息表要定期归档清理,否则数据会无限增长。
>
> 总结一句话:**本地消息表的灵魂是"本地事务 + 异步消息",业务方在本地事务里把消息写进去,靠定时扫表和消费端幂等来保证最终一致**。这是柔性事务工程化的经典模式,特别适合"上下游解耦、可容忍秒级延迟"的场景。

#8 2PC 协议阻塞问题详解 high system-design

题目

2PC 协议阻塞问题详解

我的回答

两阶段提交它的主涉是一个它的核心问题,它主涉本质上就是要确认所有人都是可以通信的,可以正常完成当前事务的。这个通信的过程需要等待,发起者与处理者都需要进行对应的处理,所以为了保证事务的正常进行,大家需要把资源都统筹起来,这个过程就会产生主涉。正常的主涉是没有什么异常的,如果中间出现了问题,比如协调者出现崩溃,导致所有的信息无法正常传递,然后网络相互间无法通讯,然后造成长时间的等待,导致资源的浪费,最终导致服务拖垮。可能会有相关的问题产生。修复方式的话,首先是要发现这个类似的问题,比如事务时间过长,监控指标中要有记载。然后如果出现了一些长事务,事务的执行时间过长,然后要做出告警。另外是对于锁的监控,也要有相关的监控。锁的量上出现了数量上的激增,或者是服务资源处上异常,对应的要能快速找到对应的方向问题。然后首先可以是释放资源,将对应的事务处理掉。然后其实本质上应该在事务那设置过期时间,让它快速的结束,而不是资源长时间等待,导致整个业务的拖垮。

点评

评分:⭐⭐⭐☆☆
优点:- 正确识别阻塞的本质:同步等待导致资源占用 - 正确识别触发场景:协调者崩溃、网络分区 - 正确识别业务影响:长等待 → 资源浪费 → 服务拖垮 - 正确提到监控维度:事务时长、锁数量 - 正确提出"超时机制"作为恢复手段
不足:- **未明确三种具体阻塞场景**:协调者崩溃在 Prepare 后/Commit 前、网络分区、参与者崩溃 - **未提到 redo log 的三种情况**:有 COMMIT / 无记录 / 只有 PREPARE——这是理解"为什么是不确定状态"的关键 - **未明确"参与者持有锁直到 Commit/Rollback"**——这是阻塞的根源 - **未解释"不确定"的含义**:参与者已执行本地事务但不知道全局事务结果 - **恢复方案太笼统**:未提到"手动清理 redo log"、"协调者故障转移"、"超时强制回滚"等具体方案 - 术语口误:"主涉"应为"阻塞" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 2PC 协议"阻塞"缺陷的精确理解——不止"会阻塞",还要讲清楚**为什么是不确定状态**、**靠 redo log 怎么恢复**、**工程上怎么缓解**。

**知识点拆解**:

1. **阻塞的本质(最关键)**
- **本质**:参与者持有资源锁 + 状态未知 + 无超时机制 → 事务永久悬挂
- **关键点**:Prepare 之后参与者已执行本地事务(但**未提交**),等待协调者指令
- 如果协调者崩溃,参与者无法判断"该 Commit 还是该 Rollback"——这就是"不确定状态"

2. **阻塞的三种场景**

```
场景 1:协调者崩溃在 Prepare 之后、Commit 之前(最关键)
├─ 参与者 1 和 2 都写完了 redo log,返回 Prepare OK
├─ 协调者写完日志、准备发 Commit 时崩溃
└─ 结果:参与者持有锁,状态不明,无法推进

场景 2:网络分区
├─ 协调者与部分参与者网络断开
├─ 部分参与者收不到指令,部分能收到
└─ 结果:分区两侧的参与者状态永远不一致

场景 3:参与者崩溃
├─ 参与者在 Prepare 阶段崩溃
└─ 结果:协调者等待参与者响应,无法推进(超时后可回滚)
```

3. **redo log 的三种情况(恢复的关键)**

```
协调者崩溃后,恢复时查 redo log:

情况 A:redo log 有 COMMIT 记录
→ 说明协调者已决定提交,发 Commit 给所有参与者

情况 B:redo log 无记录
→ 说明协调者未决定是否提交,发 Rollback

情况 C:redo log 只有 PREPARE 记录(最关键)
→ 协调者准备提交但未发 Commit
→ 协调者主动询问所有参与者:"你都 Prepare OK 了吗?"
→ 都 OK → 发 Commit;任一 Fail → 发 Rollback
```

4. **"不确定状态"的根源**
- 参与者已执行本地事务(数据已写盘)
- 但不知道全局事务结果(协调者崩溃)
- 既不能 Commit(怕其他参与者 Fail)
- 也不能 Rollback(可能其他参与者已提交)
- 只能等待协调者恢复

5. **业务影响链**
```
协调者崩溃
→ 参与者持有锁
→ 其他事务阻塞
→ 连接池耗尽
→ 服务不可用
→ 雪崩效应
```

6. **工程恢复方案**
- **超时回滚**:参与者在等待指令超过 T 秒后强制回滚(牺牲一致性换可用性)
- **手动清理**:工程师查 redo log 状态,手动发送 Commit/Rollback
- **协调者故障转移**:协调者集群 + 故障自动切换
- **2PC 变体**:用 Seata AT/TCC 等"非阻塞"方案替代
- **监控告警**:长事务告警、锁数量告警、事务悬挂告警

7. **3PC 的改进(但不完美)**
- 3PC 引入"预提交"阶段:Prepare → PreCommit → Commit
- 协调者在 PreCommit 之后崩溃,参与者可**超时提交**(因为 PreCommit 表示"协调者已决定提交")
- 但仍无法解决:协调者在 PreCommit 之前崩溃、网络分区等场景

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否能讲出 redo log 的三种情况和恢复逻辑
- **广度**:能否把 2PC 阻塞与 3PC、Seata AT 对比
- **应用**:能否设计生产环境下的阻塞检测和恢复方案

**延伸递进面试题**:
1. **"如果参与者在 Prepare 阶段崩溃了,2PC 还能恢复吗?"**
- 要点:参与者崩溃前如果已写 redo log 落盘,恢复后可通过 redo log 判断状态;如果未落盘,协调者只能超时回滚
2. **"3PC 相比 2PC 真的解决了阻塞问题吗?"**
- 要点:3PC 缓解了部分场景(PreCommit 之后崩溃),但无法解决所有场景(PreCommit 之前崩溃、网络分区)。工程实践中 3PC 并未替代 2PC
3. **"MySQL XA 的阻塞问题,生产上怎么避免?"**
- 要点:限制 XA 使用范围(仅单库多表)、设置 XA 超时时间、监控 XA 事务时长、用 Seata AT 替代跨库 XA

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,2PC 的阻塞问题是它的核心缺陷,我用三个层面来解释。
>
> **第一层:阻塞的本质**。2PC 在 Prepare 阶段,参与者已经执行了本地事务(数据已写盘到 redo log),但**没有提交**,只是在等待协调者的 Commit 或 Rollback 指令。同时参与者**持有资源锁**。如果协调者在这个窗口崩溃,参与者就陷入了"不确定状态"——既不能 Commit(怕其他参与者 Fail),也不能 Rollback(可能其他参与者已提交),只能等待协调者恢复。这就是"阻塞"的本质:**持有锁 + 状态未知 + 无超时机制**。
>
> **第二层:阻塞的三种场景**。
> 1. **协调者崩溃在 Prepare 之后、Commit 之前**(最关键):参与者都返回了 Prepare OK,协调者写完日志准备发 Commit 时崩溃
> 2. **网络分区**:协调者与部分参与者网络断开,分区两侧状态永远不一致
> 3. **参与者崩溃**:参与者在 Prepare 阶段崩溃,协调者等待响应
>
> **第三层:redo log 的恢复逻辑**(这是理解"不确定"的关键)。协调者崩溃后恢复时,会查 redo log 的状态:
> - **有 COMMIT 记录**:说明已决定提交,发 Commit
> - **无记录**:说明未决定,发 Rollback
> - **只有 PREPARE 记录**(最关键):协调者准备提交但未发 Commit。此时协调者会主动询问所有参与者:"你都 Prepare OK 了吗?"如果都 OK 就发 Commit,任一 Fail 就发 Rollback
>
> 这里有个关键点:参与者**不能自己决定** Commit 还是 Rollback,必须等协调者。这就是"不确定状态"的根源——参与者已写盘但不知道全局结果。
>
> **业务影响**:阻塞的代价是巨大的。参与者持有锁 → 其他事务阻塞 → 连接池耗尽 → 服务不可用,甚至引发雪崩效应。这就是为什么 2PC 在生产环境里慎用,尤其是 MySQL XA 这种跨库场景。
>
> **工程恢复方案**有四个:
> 1. **超时回滚**:参与者在等待指令超过 T 秒后强制回滚,牺牲一致性换可用性
> 2. **手动清理**:工程师查 redo log 状态,手动发送 Commit/Rollback
> 3. **协调者故障转移**:协调者集群 + 故障自动切换
> 4. **用 2PC 变体替代**:Seata AT 一阶段提交后释放本地锁,不阻塞;TCC 用业务锁替代数据库锁
>
> 监控层面要覆盖:长事务告警、锁数量告警、事务悬挂告警。
>
> 总结一句话:**2PC 阻塞的本质是"持有锁 + 状态未知 + 无超时",根源是 CP 系统的必然代价——为了保证一致性,牺牲可用性**。这也是为什么工程实践倾向于柔性事务(TCC、Saga、消息驱动)——用"最终一致"换"可用性"。

---

#9 详细对比 2PC 和 3PC high system-design

题目

详细对比 2PC 和 3PC

我的回答

对比两阶段提交,三阶段提交引入了预提交,本身想要解决的问题其实是在事务执行阶段,大家先预演,把事务的过程预演完,然后防止阻塞,降低阻塞,他想要做这件事情。但首先它引入了,增加了一个新的阶段,引入了新的复杂度。在预提交阶段出现了异常的问题,虽然说能够快速的把整个事务解除,不再往下进行,直接快速的结束。但它在提交阶段,真正提交的阶段,也还是会有资源出现问题,虽然会降低,但是并没有实际能解决两阶段提交真实存在的问题,只是降低了。

点评

评分:⭐⭐⭐☆☆
优点:- 正确识别 3PC 的核心改进:引入 PreCommit 阶段 - 正确识别改进目标:降低/缓解阻塞 - 正确识别代价:引入新复杂度 - 正确结论:3PC 只是降低了阻塞,没有真正解决
不足:- 未清晰描述 3PC 的三阶段:**CanCommit → DoCommit(PreCommit)→ Commit** - **未讲出 3PC 的关键机制**:参与者在 PreCommit 之后崩溃可"**超时提交**"(因为 PreCommit 表示协调者已决定提交) - 未解释 3PC 仍不完美:协调者在 PreCommit 之前崩溃、**网络分区**场景 - 未提到核心 trade-off:3PC 用"额外一轮通信"换"超时提交的可能性" - 未说明为什么 3PC 没有替代 2PC:网络分区下 3PC 也无法保证一致性 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 3PC 协议"预提交阶段"的精确理解——不止"3PC 缓解了阻塞",还要讲清楚**PreCommit 后参与者的状态变化**、**为什么网络分区下 3PC 也失效**。

**知识点拆解**:

1. **3PC 的三阶段流程**

```
阶段 1:CanCommit(询问)
├─ 协调者问:"你能提交吗?"
├─ 参与者执行事务但不提交,写 undo log,**不持锁**
└─ 返回 CanCommit OK / Fail

阶段 2:DoCommit(预提交)
├─ 全部 OK 后协调者发 PreCommit
├─ 参与者**提交本地事务**(释放本地锁!)
└─ 等待最终 Commit/Rollback 指令

阶段 3:Commit(提交)
├─ 参与者收到 Commit 或**超时**
├─ 提交最终状态
└─ 事务结束
```

2. **3PC 的核心改进(PreCommit 的作用)**
- **关键差异**:PreCommit 之后参与者**已提交本地事务**(不是 2PC 那样"持有锁等待")
- **超时提交机制**:如果协调者崩溃,参与者可以**超时提交**(因为 PreCommit 表示协调者已决定提交)
- **效果**:缓解了 2PC 的"阻塞"问题

3. **3PC 的新问题(仍未完美)**

```
问题 1:协调者在 PreCommit 之前崩溃
→ 参与者只能超时回滚(同 2PC,未改进)

问题 2:网络分区
→ PreCommit 发出后网络断开
→ 部分参与者提交、部分回滚
→ 不一致(3PC 无法解决)

问题 3:参与者超时提交后协调者恢复
→ 协调者可能发 Rollback
→ 已提交的参与者无法回滚
→ 不一致
```

4. **3PC 与 2PC 的对比表**

| 维度 | 2PC | 3PC |
|------|-----|-----|
| 阶段数 | 2 | 3 |
| 锁持有时间 | 全程 | **PreCommit 后释放** |
| 协调者崩溃 | 阻塞 | PreCommit 后可超时提交 |
| 网络分区 | 不一致 | **仍不一致** |
| 通信开销 | 2 轮 | 3 轮 |
| 工程实践 | 常用(XA) | **几乎不用** |

5. **为什么 3PC 没有替代 2PC**
- 网络分区场景下 3PC 也无法保证一致性
- 引入了更多复杂度和通信开销
- 工程上柔性事务(TCC、Seata AT)比 3PC 更实用
- 3PC 主要是学术上的改进,工程上无优势

6. **工程实践的现实选择**
- **强一致**:2PC/XA(接受阻塞代价)或 TCC/Seata AT(非阻塞)
- **最终一致**:Saga / 本地消息表 / 事务消息
- **3PC**:几乎不用,学术为主

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否理解 PreCommit 后参与者"已提交本地事务"是关键
- **广度**:能否对比 2PC/3PC 在阻塞和网络分区下的差异
- **应用**:能否解释为什么 3PC 没有替代 2PC

**延伸递进面试题**:
1. **"3PC 在 PreCommit 之后,参与者超时提交,但协调者恢复后发 Rollback,会怎样?"**
- 要点:已提交的参与者无法回滚,造成不一致。这是 3PC 的核心缺陷
2. **"如果网络分区发生在 PreCommit 阶段,3PC 会怎样?"**
- 要点:分区两侧的参与者状态永远不一致,3PC 无法解决网络分区
3. **"为什么工程实践选择 TCC/Seata AT 而不是 3PC?"**
- 要点:3PC 仍不完美,TCC/Seata AT 通过"业务补偿"和"全局锁"实现非阻塞,更实用

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,3PC 是 2PC 的改进版,引入"预提交"阶段来缓解 2PC 的阻塞问题,但并没有真正解决。我用三个阶段来解释。
>
> **3PC 的三阶段流程**:
> 1. **CanCommit(询问)**:协调者问"你能提交吗?",参与者执行事务但不提交,写 undo log,**不持锁**,返回 CanCommit OK 或 Fail
> 2. **DoCommit(预提交)**:全部 OK 后协调者发 PreCommit,参与者**提交本地事务**(释放本地锁!),等待最终指令
> 3. **Commit(提交)**:参与者收到 Commit 或**超时**,提交最终状态
>
> 这里的关键改进在第二阶段——**PreCommit 之后参与者已提交本地事务**,不是 2PC 那样"持有锁等待协调者"。所以如果协调者在 PreCommit 之后崩溃,参与者可以**超时提交**,因为 PreCommit 已经表示"协调者决定提交"了。这就是 3PC 缓解阻塞的核心机制。
>
> 但 3PC 仍有三个问题没解决:
> 1. **协调者在 PreCommit 之前崩溃**:参与者只能超时回滚,同 2PC
> 2. **网络分区**:PreCommit 发出后网络断开,分区两侧的参与者状态永远不一致
> 3. **参与者超时提交后协调者恢复**:协调者可能发 Rollback,已提交的参与者无法回滚,造成不一致
>
> 这里有个关键点:3PC 用"额外一轮通信"换"超时提交的可能性",但代价是引入了更多复杂度和通信开销。更关键的是,**网络分区场景下 3PC 也无法保证一致性**——这是 CAP 定理的必然限制,3PC 无法突破。
>
> 所以工程实践中 3PC 几乎不用,主要是学术上的改进。生产环境要解决分布式事务,主流选择是:
> - **强一致**:2PC/XA(接受阻塞)或 TCC/Seata AT(非阻塞)
> - **最终一致**:Saga / 本地消息表 / 事务消息
>
> 总结一句话:**3PC 用"预提交+超时提交"缓解了 2PC 的阻塞问题,但无法解决网络分区下的不一致,工程上被 TCC/Seata AT 等柔性事务替代**。这也印证了一个工程原则——**协议设计的完美主义往往让位于业务实践的实用性**。

---

#10 跨境支付平台方案设计 high system-design

题目

跨境支付平台方案设计

我的回答

由于是资金交互,所以这里边在资金扣除和资金入账以及锁定汇率这三个阶段应该是强一致的。要求必须要么都完成,要么都完都不完成,不能出现中间态。这块我推荐使用 RPC 去强一致的执行,确保同时完成或者同时都不完成。然后像基于审计追踪这些东西可以通过异步的去抛,写到本地消息表或者是消息事务消息中,去保证一个最终的一致性,并且确保这个事情最终能完成,在整个事务结束的时候。为了确保资金的完整性,在提交请求的时候,确认一个唯一的密等的链路。提交转汇请求,确认一个唯一的 ID,整个确认整个订单的执行流程。然后在每个阶段,包括确定外汇汇率、本币扣减、外币入账,这一套每个阶段记录对应的事务日志,确保单向执行,做状态机的处理,每个阶段进行对应的事务状态记录。确保不会出现同一个请求多次执行的问题。并且生成的事务状态记录也可以用来提供给审计生成做对账。由于我们在前置的时候考虑在对账流水使用异步的方式去生成,可以依据对应的消息队列信息、MQ 这些去异步的生成流水。如果流水出现问题,跟金额对不上,可以基于刚刚生成的事务状态日志去做回查对账,最终保证整个数据的强一致变更。

点评

评分:⭐⭐⭐⭐☆
优点:- **架构分层正确**:主链路(扣款/入账/锁汇率)强一致 + 副链路(审计/流水)最终一致 - **正确识别模式**:强一致用同步、最终一致用本地消息表/事务消息 - **幂等设计到位**:唯一 ID 贯穿全链路,防止重复执行 - **状态机处理**:每个阶段记录事务状态,支持单向执行 - **对账兜底**:事务状态记录 + 回查对账,作为最终防线 - **MQ 异步化**:审计流水通过 MQ 异步生成,不阻塞主链路
不足:- **术语错误**:"RPC 去强一致的执行"——RPC 不是分布式事务协议,应为 **TCC / Seata AT / XA** - **未指定具体分布式事务模式**:主链路用 TCC?Seata AT?XA?——这是方案设计的关键决策 - **未处理外部汇率服务**:第三方依赖的幂等、超时、补偿怎么处理? - **未提 TCC 三大坑**(如果用 TCC):空回滚、幂等、悬挂 - **未提资金安全细节**:账户冻结(冻结余额而非直接扣款)、双花防护、限额检查 - **未提审计细节**:全链路追踪(trace_id)、操作流水、监管合规 - **未提延迟优化**:汇率预热缓存、批量处理、异步通知 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察分布式事务知识的**融会贯通**——能否把前 9 题的概念应用到真实业务场景,做完整的方案设计。

**知识点拆解**:

1. **业务拆解(最关键)**
```
主链路(强一致):扣款 + 锁汇率 + 入账
副链路(最终一致):审计流水 + 监管上报 + 用户通知
```
- 主链路必须强一致:资金不能中间态
- 副链路可最终一致:审计/通知可容忍秒级延迟

2. **主链路方案:TCC 模式**
- **为什么用 TCC**:资金场景需要高可控性、可审计性,业务补偿可控
- **Try 阶段**:冻结 A 账户本币余额 + 锁定汇率(外部服务幂等)+ 冻结 B 账户外币额度
- **Confirm 阶段**:真实扣款 A + 真实入账 B + 解锁汇率
- **Cancel 阶段**:释放冻结 + 取消汇率锁定
- **三大坑**:空回滚(Try 标记表)、幂等(业务单号唯一键)、悬挂(事务状态表检查)

3. **副链路方案:本地消息表 / RocketMQ 事务消息**
- **本地消息表**:本地事务写审计记录 + 消息表,定时扫表投递
- **RocketMQ 事务消息**:half message + 回查,业务方代码更简洁
- **选型**:业务方运维能力强 → 本地消息表;想用成熟 MQ → RocketMQ 事务消息

4. **外部汇率服务处理**
- **幂等键**:基于订单 ID + 用户 ID,防止重复锁定汇率
- **超时重试**:指数退避 1s → 2s → 4s,最多 3 次
- **缓存预热**:常用汇率预加载到 Redis,TTL 5-10 秒
- **降级方案**:汇率服务不可用时,使用缓存汇率 + 标记"需人工确认"

5. **资金安全设计**
- **账户级冻结**:Try 时冻结余额(不实际扣款),Confirm 时真实扣款
- **双花防护**:基于账户余额 + 业务单号唯一键,防止同一笔钱被多次使用
- **限额检查**:单笔/单日/单月限额,风控前置
- **资金隔离**:用户资金、平台资金、清算资金分账户管理

6. **审计追踪设计**
- **trace_id 贯穿全链路**:所有日志、流水、消息都带 trace_id
- **操作流水表**:每步操作记录操作人、时间、参数、结果、状态
- **监管合规**:交易明细、反洗钱检查、跨境合规
- **不可篡改**:审计日志写 WORM 存储(Write Once Read Many)

7. **延迟优化设计**
- **汇率预热**:常用汇率缓存 5-10 秒,减少外部调用
- **批量处理**:批量入账、批量审计,减少事务次数
- **异步通知**:用户通知、监管上报走 MQ,不阻塞主链路
- **连接池优化**:数据库、RPC、MQ 连接池调优

8. **对账兜底设计**
- **日终对账**:对比 TCC 状态 + 账户流水 + 汇率服务记录
- **实时对账**:关键交易实时比对(如金额大于阈值)
- **差错账**:发现不一致自动生成差错账,人工处理
- **对账系统架构**:独立对账服务 + 定时任务 + 告警

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否把分布式事务模式(TCC/消息)应用到真实业务
- **广度**:能否覆盖业务拆解、资金安全、审计、性能、对账五个维度
- **应用**:能否处理外部依赖(汇率服务)的幂等、超时、补偿

**延伸递进面试题**:
1. **"如果外部汇率服务在 Try 阶段超时,TCC 会怎么处理?"**
- 要点:Try 超时后发起 Cancel(空回滚),但 Cancel 也要考虑汇率服务超时;最终靠对账系统发现"已冻结但未扣款"的状态,人工处理
2. **"资金安全怎么防止'双花'?"**
- 要点:基于账户余额 + 业务单号唯一键;账户级冻结(Try 冻结、Confirm 扣款);风控前置检查
3. **"审计日志和事务状态记录的区别?"**
- 要点:事务状态记录是 TCC 框架的元数据(用于恢复),审计日志是业务操作记录(用于合规);两者都需要,用途不同

第三部分:面试者参考回答(300-600 字口语化)

> 面试官您好,跨境支付这种资金场景,我的方案是"**主链路强一致 + 副链路最终一致 + 对账兜底**"的分层设计。
>
> **业务拆解**:
> - **主链路(强一致)**:扣款 A + 锁定汇率 + 入账 B,这三步必须原子性完成
> - **副链路(最终一致)**:审计流水、监管上报、用户通知,可以容忍秒级延迟
>
> **主链路方案:TCC 模式**。资金场景需要高可控性、可审计性,TCC 最适合。
> - **Try**:冻结 A 账户本币余额(不实际扣款)+ 锁定汇率(外部服务幂等)+ 冻结 B 账户外币额度
> - **Confirm**:真实扣款 A + 真实入账 B + 解锁汇率
> - **Cancel**:释放冻结 + 取消汇率锁定
> - **三大坑处理**:空回滚(Try 标记表检查)、幂等(业务单号唯一键)、悬挂(事务状态表检查)
>
> **副链路方案:本地消息表 + RocketMQ**。主事务提交时写审计记录 + 消息表,定时扫表投递到 MQ;审计流水、监管上报、用户通知都走 MQ 异步处理,不阻塞主链路。
>
> **外部汇率服务处理**:
> - **幂等键**:基于订单 ID + 用户 ID,防止重复锁定
> - **超时重试**:指数退避 1s → 2s → 4s,最多 3 次
> - **缓存预热**:常用汇率预加载到 Redis,TTL 5-10 秒
> - **降级**:汇率服务不可用时,使用缓存汇率 + 标记"需人工确认"
>
> **资金安全设计**:
> - **账户级冻结**:Try 时冻结余额,Confirm 时真实扣款,防止双花
> - **唯一键**:基于账户余额 + 业务单号,防止同一笔钱被多次使用
> - **限额检查**:单笔/单日/单月限额,风控前置
>
> **审计追踪**:
> - **trace_id 贯穿全链路**:所有日志、流水、消息都带 trace_id
> - **操作流水表**:每步操作记录操作人、时间、参数、结果
> - **不可篡改**:审计日志写 WORM 存储
>
> **延迟优化**:
> - **汇率预热**:常用汇率缓存,减少外部调用
> - **批量处理**:批量入账、批量审计
> - **异步通知**:用户通知、监管上报走 MQ
>
> **对账兜底**:
> - **日终对账**:对比 TCC 状态 + 账户流水 + 汇率服务记录
> - **差错账**:发现不一致自动生成差错账,人工处理
>
> 总结一句话:**资金场景的核心原则是"主链路 TCC 保证强一致,副链路消息保证最终一致,对账系统兜底防错"**。这也是为什么支付宝、微信支付等核心金融系统都用这种"组合拳",而不是某个单一方案。

---

📚 查看全部面试记录