TCC 的空回滚、悬挂、幂等问题分别是什么?怎么解决?
简化版
TCC 在网络异常/超时/乱序下有三个必须处理的坑:空回滚(Try 没执行,却先收到了 Cancel)、悬挂(Cancel 已执行,迟到的 Try 又来了,预留的资源再没人释放)、幂等(Try/Confirm/Cancel 因重试被调用多次,要保证只生效一次)。统一解法是引入一张事务控制表记录每笔事务的执行状态,靠它判断「Try 有没有执行过」「是否已 Cancel」,从而拦住空回滚和悬挂,并实现幂等。
详细版
① 空回滚(Empty Rollback):事务管理器调 Try 时因网络超时失败(但 Try 其实没执行到),于是进入第二阶段调 Cancel。此时 Cancel 面对的是「一个从没 Try 过的事务」,如果它傻乎乎去释放资源,可能出错(释放了不存在的冻结)。
- 解决:Cancel 时先查事务控制表,如果没有 Try 记录,说明是空回滚,直接返回成功,不做实际释放。
② 悬挂(Hang / 空悬挂):承接空回滚——Cancel 先执行了(空回滚),结果那个超时的 Try 请求姗姗来迟又到了,把资源预留(冻结)了。但此时第二阶段已经结束(已 Cancel),再没有 Confirm/Cancel 会来释放这笔冻结,资源被永久占用(悬挂)。
- 解决:Try 执行前先查事务控制表,如果发现该事务已经 Cancel 过了,就拒绝执行 Try,防止迟到的 Try 预留资源。
③ 幂等(Idempotency):网络重试会让 Try/Confirm/Cancel 被调用多次。若不幂等,Confirm 可能扣两次款、Cancel 可能解冻两次。
- 解决:每个方法执行前查事务控制表的状态,已执行过则直接返回成功,保证只生效一次。
完整版教学
一、三个问题的共同根源:网络的不确定性 + 乱序
TCC 的三大问题全都源于分布式的老毛病——网络会超时(不知道对方执行没)、请求会重试、消息会乱序。正常顺序应该是 Try → Confirm/Cancel,但现实中:
- Try 的请求可能在网络里丢了或超时(发起方以为失败,实际可能到达也可能没到达)。
- 因为「以为 Try 失败」,发起方发起 Cancel。
- 那个「超时的」Try 可能其实还在网络里飘着,晚点才到。
于是就有了「Try 没成功却 Cancel(空回滚)」和「Cancel 之后 Try 才到(悬挂)」这些乱序场景。加上重试导致的重复调用,就凑齐了三大问题。
二、事务控制表:一张表解决三个问题
核心解法是给每个参与者维护一张事务控制表(或叫事务日志表),记录每笔全局事务在本参与者的执行状态。典型字段:全局事务ID、分支事务ID、状态(Tried/Confirmed/Cancelled)、时间。有了它:
- 防空回滚:Cancel 时先查表。没有 Try 记录 → 是空回滚 → 插入一条「已 Cancel」记录,直接返回成功,不做实际资源释放。
- 防悬挂:Try 时先查表。已存在 Cancel 记录 → 说明二阶段已结束,Try 迟到了 → 直接拒绝,不预留资源。
- 保幂等:每个方法进来先查状态,已是目标状态(Confirm 时已 Confirmed、Cancel 时已 Cancelled)→ 直接返回成功,不重复执行。
这张表的插入/更新和业务操作要在同一个本地事务里,才能保证「状态记录」和「资源变更」原子一致。
三、逐个场景走一遍(配合控制表)
空回滚:Try 超时没执行 → Cancel 到达 → 查表无 Try 记录 → 写一条 Cancelled 记录,返回成功(不释放资源,因为本来就没冻结)。✅
悬挂:(接上)迟到的 Try 到达 → 查表发现已有 Cancelled 记录 → 拒绝 Try,不冻结资源。✅ 若没有这道检查,Try 会冻结资源且永远等不到二阶段来释放。
幂等:Confirm 重复到达 → 查表发现已 Confirmed → 直接返回成功。✅ 避免重复扣款。
记忆点:三个问题一张表搞定——Cancel 先看有没有 Try(防空回滚)、Try 先看有没有 Cancel(防悬挂)、所有方法先看当前状态(保幂等)。本质是用持久化的状态记录来对抗网络乱序和重试。
四、为什么这是 TCC 的「必修课」
面试问 TCC,只讲 Try/Confirm/Cancel 三个方法是浅层;能讲出这三大边界问题及事务控制表解法,才说明真正落地过。因为在生产环境,网络抖动、超时、重试是常态,不处理这三个问题的 TCC 会在异常场景下资源泄漏(悬挂)、数据错误(重复执行)、报错(空回滚)。Seata 的 TCC 模式内置了防悬挂和空回滚的处理,也是基于类似的事务记录机制。
五、TCC 三大问题落地自检
TCC 分支事务上线前要检查事务状态表是否能表达 Try 成功、Cancel 完成、Confirm 完成、空回滚和悬挂拒绝。比如同一个 xid+branchId 第一次收到 Cancel 时,即使没有 Try 记录,也要插入一条 CANCELED 状态;后续 Try 到达看到 CANCELED 必须拒绝,不能再冻结资源。这个自检能把“空回滚”和“悬挂”串起来,而不是只背两个名词。
六、常见误区与追问
这道题不能只背概念,要把「TCC 空回滚、悬挂、幂等」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | TCC 要重点防空回滚、悬挂和重复 Confirm/Cancel,核心靠事务状态记录和幂等控制 | 不要停在名词解释 |
| 流程机制 | 收到 Try/Confirm/Cancel -> 检查事务状态表 -> 空回滚写入 Cancel 标记 -> Try 发现已 Cancel 则拒绝 -> 重复请求按状态幂等返回 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | Cancel 先于 Try 到达时,如果没有 Try 记录,后续 Try 不能再成功,否则就是悬挂 | 分布式事务没有免费强一致,性能、可用性和业务侵入一定要取舍 |
TCC 空回滚、悬挂、幂等 面试拆解:
1. 收到 Try/Confirm/Cancel
2. 检查事务状态表
3. 空回滚写入 Cancel 标记
4. Try 发现已 Cancel 则拒绝
5. 重复请求按状态幂等返回
记忆钩子:先说本地事务失效的原因,再拆协调、补偿、消息、幂等、对账和回滚边界;回答时要紧扣「TCC 空回滚、悬挂、幂等」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:空回滚直接忽略就好。 忽略后后续 Try 可能成功,形成悬挂,应该记录已回滚状态。
- 误区:悬挂只是调用顺序问题。 悬挂会让一个已取消的事务分支又成功预留资源,破坏一致性。
- 误区:幂等只做 Confirm 就够。 Try、Confirm、Cancel 都可能重复到达,都要幂等。
- 追问:空回滚怎么产生? Try 请求丢失或超时,事务管理器触发 Cancel 先到分支。
- 追问:如何防悬挂? Cancel 空回滚时写状态,Try 发现 Cancel 状态直接拒绝。
- 追问:状态表要记录什么? 全局事务 ID、分支 ID、阶段状态、幂等结果和更新时间。
七、加强记忆
TCC 三大问题都源于网络超时+重试+乱序:空回滚(Try 没成功就来了 Cancel)、悬挂(Cancel 后迟到的 Try 预留了没人释放的资源)、幂等(方法被重试多次)。统一用事务控制表记录每笔事务状态:Cancel 先查有无 Try(无则空回滚,返回成功不释放)、Try 先查有无 Cancel(有则拒绝,防悬挂)、每个方法先查状态(已执行则跳过,保幂等)。控制表更新与业务变更须同一本地事务。