欢迎光临
我们一直在努力

孤舟笔记 分布式与微服务篇十六 TCC中的悬挂问题是什么?怎么解决?这个坑踩过才算真懂分布式事务

文章目录

    • 先说结论
    • 问题场景:网络超时惹的祸
    • 空回滚:Cancel 时 Try 没执行
    • 悬挂:Cancel 后 Try 才到
    • 解决方案:事务状态表
    • Seata 1.5.1+ 的解决方案
    • 回答技巧与点评
      • 加分回答
      • 面试官点评

个人网站

TCC(Try-Confirm-Cancel)看起来很完美——Try 冻结资源,Confirm 确认,Cancel 取消。但实际生产中有个隐藏的坑:悬挂。面试官问这题,说明他想考察你对 TCC 的理解不只是理论层面,而是真正落地过的。他想听的是:悬挂是怎么产生的、空回滚是什么、以及怎么解决?

先说结论

维度说明
悬挂 Cancel 比 Try 先执行,导致资源被冻结后永远无法释放
空回滚 Try 没执行就收到了 Cancel 请求
根因 网络超时导致 Try 请求延迟,TC 提前触发了 Cancel
解决方案 在业务表中记录事务状态,Try 检查是否已 Cancel,Cancel 检查是否已 Try

|一句话记住:悬挂就像"先收到退课通知,后收到选课确认"——课退了但系统还记着你选了,资源被白白占着"

问题场景:网络超时惹的祸

TCC 的正常流程:

TM → Try(冻结) → Confirm(确认) ✅ 正常
TM → Try(冻结) → Cancel(取消) ✅ 正常

但网络超时可能打乱顺序:

场景1:空回滚
Try请求网络丢包 → 没到达服务B
TC等待超时 → 触发Cancel
Cancel到达服务B → 但Try根本没执行过! 👈
→ Cancel要回滚一个不存在的冻结 → 空回滚

场景2:悬挂
Try请求网络延迟(没丢,只是慢)
TC等待超时 → 触发Cancel
Cancel先到达服务B → 执行空回滚
Try延迟到达服务B → 执行冻结 👈
→ 资源被冻结了,但不会再有Confirm/Cancel → 悬挂!

悬挂的本质:Cancel 先于 Try 执行了,Cancel 做了空回滚(啥也没回滚),然后 Try 才到,冻结了资源——但事务已经结束了,没人会再来 Confirm 或 Cancel,资源永远被冻结。

空回滚:Cancel 时 Try 没执行

// 没有防悬挂的 Cancel
public boolean cancelDeduct(String accountId, BigDecimal amount) {
// Try 没执行,frozen_amount = 0
// 直接解冻 → 解冻0元 → 啥也没干
accountMapper.unfreeze(accountId, amount); // 👈 空回滚,看似无害
return true;
}

空回滚本身不是大问题——没有冻结就没有需要释放的。但如果不记录空回滚,后续的 Try 就不知道 Cancel 已经执行过了,这才是悬挂的根源。

悬挂:Cancel 后 Try 才到

// 没有防悬挂的 Try
public boolean tryDeduct(String accountId, BigDecimal amount) {
// 不知道 Cancel 已经执行过
accountMapper.freeze(accountId, amount); // 👈 冻结了,但没人来释放!
return true;
}

资源永远被冻结——这就是悬挂,也叫"资源泄漏"。

解决方案:事务状态表

核心思路:用一张事务状态表记录每个分支事务的执行状态。

— 事务状态表
CREATE TABLE tcc_transaction (
xid VARCHAR(128), — 全局事务ID
branch_id VARCHAR(128), — 分支事务ID
status VARCHAR(16), — 状态:trying/cancelled
PRIMARY KEY (xid, branch_id)
);

防悬挂的 Try:

public boolean tryDeduct(String xid, String branchId, String accountId, BigDecimal amount) {
// 1. 检查是否已经 Cancel 过
TccTransaction tx = tccMapper.findByXidAndBranch(xid, branchId);
if (tx != null && "cancelled".equals(tx.getStatus())) {
return false; // 👈 Cancel 已执行,拒绝 Try(防悬挂)
}

// 2. 记录 Trying 状态
tccMapper.insert(xid, branchId, "trying");

// 3. 执行冻结
accountMapper.freeze(accountId, amount);
return true;
}

防空回滚的 Cancel:

public boolean cancelDeduct(String xid, String branchId, String accountId, BigDecimal amount) {
// 1. 检查是否执行过 Try
TccTransaction tx = tccMapper.findByXidAndBranch(xid, branchId);

if (tx == null) {
// Try 没执行过 → 记录 Cancelled 状态(防空回滚+防悬挂) 👈
tccMapper.insert(xid, branchId, "cancelled");
return true;
}

// 2. Try 已执行 → 正常回滚
accountMapper.unfreeze(accountId, amount);
tccMapper.updateStatus(xid, branchId, "cancelled");
return true;
}

关键点:Cancel 时即使 Try 没执行,也要记录 cancelled 状态。这样后续到达的 Try 才能发现 Cancel 已经执行过,拒绝冻结资源。

Seata 1.5.1+ 的解决方案

Seata 内置了防悬挂和空回滚的处理:

# Seata 配置
seata:
tcc:
# 防悬挂:Try 时检查是否已 Cancel
# 空回滚:Cancel 时检查是否已 Try
# Seata 自动维护事务状态表 👈

但如果你自己实现 TCC,必须手动处理这两个问题。

TCC 悬挂与空回滚全景

问题
├── 空回滚 —— Cancel时Try没执行过
├── 悬挂 —— Cancel先于Try执行,Try后到冻结资源
└── 根因 —— 网络超时导致Try延迟

解决方案:事务状态表
├── Try —— 检查是否已cancelled,是则拒绝
├── Cancel —— 记录cancelled状态(即使Try没执行)
└── 状态流转 —— trying → confirmed/cancelled

Seata内置
├── 自动维护事务状态
├── 防悬挂检查
└── 空回滚处理

口诀:网络超时惹的祸,Cancel可能先于Try;
空回滚是Cancel没东西回滚,悬挂是Try后到冻结资源;
事务状态表来帮忙,Try检查Cancel来过没;
Cancel记录状态防悬挂,Seata内置自动处理

回答技巧与点评

标准回答:TCC 的悬挂问题是:网络超时导致 Cancel 先于 Try 执行,Cancel 做了空回滚后,延迟的 Try 才到达并冻结了资源,但事务已结束不会再有 Confirm/Cancel,资源永远无法释放。解决方式是用事务状态表:Try 时检查是否已 Cancel(已 Cancel 则拒绝),Cancel 时记录 Cancelled 状态(即使 Try 没执行也记录),这样后续 Try 就能感知到 Cancel 已执行。

加分回答

  • 幂等性也要考虑:除了悬挂和空回滚,TCC 的 Confirm/Cancel 也可能重复执行(网络重试)。所以 Confirm/Cancel 也要做幂等——通过事务状态表判断是否已执行过
  • Seata 的全局锁 + TCC:Seata 1.5+ 把 AT 模式的全局锁和 TCC 结合,Try 阶段就注册全局锁,防止其他事务修改同一数据,进一步保证一致性
  • TCC 的三大问题:空回滚、悬挂、幂等性,合称 TCC 的"三座大山"。面试时三个都讲出来,说明你真正落地过 TCC
  • 面试官点评

    这道题考的是你对 TCC 落地难点 的理解。最忌讳的回答是只知道 Try/Confirm/Cancel 三个接口——面试官想听的是 网络异常下的边界情况。能讲清悬挂的因果链(超时→Cancel先到→Try后到→资源泄漏)和解决方案(状态表),就是高分回答。

    原文阅读


    内容有帮助?点赞、收藏、关注三连!评论区等你 💪

    赞(0)
    未经允许不得转载:171主机测评 » 孤舟笔记 分布式与微服务篇十六 TCC中的悬挂问题是什么?怎么解决?这个坑踩过才算真懂分布式事务
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址