什么是分布式事务的防悬挂,空回滚?
- 标签:Spring Boot、Spring Cloud
🏛 业务架构师
这两个词出自 TCC 分布式事务(Try-Confirm-Cancel)的异常场景,是 TCC 落地时必须处理的工程问题:
空回滚(Null Rollback):Try 请求因网络问题没到达参与方,但 TM 却判定事务失败发起了 Cancel——参与方收到 Cancel 时"我根本没 Try 过",如果直接执行 Cancel 逻辑就会错乱(比如没冻结额度却扣减额度)。防御方案:Cancel 先检查 Try 是否执行过(事务控制表记录 Try 状态),没执行过就"空回滚"直接返回成功,并记录防重标记。
悬挂(Hanging):分支的 Cancel 比 Try 先到(网络延迟导致 Try 报文还在路上),Cancel 空回滚后返回,迟到的 Try 这时才到达并执行了资源预留——这笔预留再也没有人来 Confirm/Cancel,业务资源被"悬挂"占用(额度冻着永不释放)。防御方案:Try 执行前检查"是否已经回滚过"(防悬挂表),回滚过则拒绝执行 Try。
工程落地:
- 事务控制表是标配:每个分支记录 xid + 分支状态(TRIED/CONFIRMED/CANCELLED),Try/Confirm/Cancel 全程查表防重防悬挂;
- Seata TCC 模式已内置防悬挂与空回滚保护(
useTCCFence=true开启事务防悬挂表),自研 TCC 就要自己扛这些边界; - 一切防御的基石是三个方法都幂等。
架构师视角:这就是我一直强调的"TCC 业务侵入大"的具体含义——不只多写三个接口,还要处理这些网络异常边界的防御逻辑。选 TCC 前先评估团队是否能扛住这类细节的工程质量,扛不住就用 AT 或最终一致。
🏗 系统架构师
TCC 的两类经典异常场景:
- 空回滚:Try 因网络未到达,事务管理器超时后驱动 Cancel 先于 Try 执行——Cancel 回滚了一个不存在的资源;若直接成功返回,迟到的 Try 到达后又把资源真扣掉,状态错乱;
- 悬挂:空回滚之后,迟到的 Try 才到达并执行成功——资源被 Try 占用却永远等不来 Confirm/Cancel,事务「悬挂」在中间态。
解法:事务防护表(fence 表):
- Cancel 执行前查防护表:无 Try 记录 → 插入「已回滚」标记后直接返回成功(空回滚安全返回);
- Try 执行前查防护表:发现「已回滚」标记 → 拒绝执行(防悬挂);
- Seata 1.5.1+ 的 TCC fence 表内置了这套逻辑(记录状态加唯一约束)。
架构视角:根因是分布式网络下消息可能迟到、重放、乱序。任何补偿类方案(TCC/Saga/MQ 重试)都必须满足三件事:幂等(重复执行结果不变)、状态守卫(执行前置检查)、唯一约束兜底。做资金系统时我要求所有补偿接口按「可乱序、可重入」设计,并用故障注入(延迟 Try)实际验证过。
💻 全栈开发专家
TCC 场景的两大经典异常,根因是网络乱序/超时重试导致 Cancel 或 Try 单独到达:
- 空回滚:Try 因网络没到达,事务协调器判定失败发起 Cancel——但 Try 的资源预留根本没执行,Cancel 却去"回滚一个不存在的事务"(比如解冻了没冻结的金额)
- 悬挂:空回滚的 Cancel 执行完之后,堵在网络里的 Try 才到达并成功执行,预留的资源(冻结金额)永远没有 Cancel 来释放——事务"悬"在中间态,钱冻着没人管。防悬挂就是防这个现象
解法标准且唯一思路:事务控制表 + 状态机:
sql
CREATE TABLE tcc_tx (
xid VARCHAR(64), -- 全局事务ID
branch_id VARCHAR(64),
state TINYINT, -- 1:tried 2:cancelled 3:confirmed
UNIQUE KEY uk_branch (xid, branch_id)
);- Cancel 先执行:查无记录 → 说明 Try 没来 → 插入 cancelled 占位记录后直接返回成功(防空回滚),并以此拒绝迟到的 Try
- Try 执行前:先查状态,发现已是 cancelled → 直接拒绝(防悬挂)
- Confirm/Cancel 都要幂等(可能重试多次)
Seata 1.5.1+ 的 TCC 内置了这套防护(fence 表,@TwoPhaseBusinessAction(useTCCFence = true) 开启);自研 TCC 必须把这层做进框架而不是散在业务里。