深入分析 Seata TCC 模式的空回滚与悬挂问题及解决方案
Seata 的 TCC(Try-Confirm-Cancel)模式是分布式事务中常用的解决方案,但在实际应用中,可能会遇到 空回滚 和 悬挂 问题。本文将以一个包含 A、B、C 三个分支事务的场景为例,清晰地分析这两个问题的时序成因,并提供解决方案。
场景设定
假设一个订单支付场景:
- 全局事务:用户下单并支付。
- 分支事务 A:扣减库存。
- 分支事务 B:扣减账户余额。
- 分支事务 C:记录订单状态。
TCC 模式下,每个分支事务有 Try、Confirm、Cancel 三个阶段,全局事务协调者(TC)负责协调。
一、空回滚问题
1.1 问题定义
空回滚是指 TCC 的二阶段 Cancel 被调用,但一阶段 Try 并未执行,导致资源被错误回滚。
1.2 时序分析
正常时序:
- TC 发起全局事务,生成 XID。
- A、B、C 的 Try 依次执行:
- A.Try:锁定 10 件库存。
- B.Try:锁定 100 元余额。
- C.Try:记录订单为"待确认"。
- TC 提交全局事务,触发 A、B、C 的 Confirm。
异常时序(空回滚):
- TC 发起全局事务。
- A.Try 执行成功,锁定库存。
- B.Try 因网络超时未完成(未锁定余额)。
- TC 判断全局事务超时,触发回滚。
- A.Cancel 执行,释放库存。
- B.Cancel 执行,但 B.Try 未执行过,此时 B 的 Cancel 无意义(空回滚)。
- C.Try 未执行,C.Cancel 也为空回滚。
1.3 问题影响
- B 和 C 的 Cancel 被调用,但由于 Try 未执行,可能会记录错误的日志或触发无意义的清理逻辑。
1.4 解决方案
1.4.1 事务状态记录
在每个分支事务的 Try 阶段记录状态:
- 数据库表:
tcc_status (xid, branch_id, status)
。 - Try 执行成功后,插入状态
TRY_SUCCESS
。 - Cancel 前检查状态,若无
TRY_SUCCESS
,跳过回滚。
示例伪代码:
java
if (tccStatusRepository.findStatus(xid, branchId) != "TRY_SUCCESS") {
return; // 跳过空回滚
}
1.4.2 TC 优化
TC 在触发 Cancel 前,查询各分支的 Try 执行状态(通过 Seata 的 branch_table
),只对 Try 成功的分支发起回滚。
二、悬挂问题
2.1 问题定义
悬挂是指 TCC 的二阶段 Cancel 先于 Try 执行,导致 Try 在 Cancel 后仍尝试预留资源,造成状态不一致。
2.2 时序分析
正常时序:
- TC 发起全局事务。
- A.Try、B.Try、C.Try 依次执行。
- TC 提交或回滚,触发 Confirm 或 Cancel。
异常时序(悬挂):
- TC 发起全局事务。
- A.Try 执行成功,锁定库存。
- B.Try 未执行(网络延迟)。
- TC 判断超时,触发回滚。
- B.Cancel 先到达 B 服务(此时 B 无资源锁定)。
- B.Try 延迟到达,锁定 100 元余额。
- A.Cancel 执行,释放库存。
- 结果:库存已释放,但余额仍被锁定,出现悬挂。
2.3 问题影响
- 余额被错误锁定,系统状态不一致,用户无法再次使用这部分资源。
2.4 解决方案
2.4.1 前置状态检查
Try 执行前检查事务状态:
- 若
tcc_status
中已有CANCELLED
,拒绝 Try 执行。
java
if (tccStatusRepository.findStatus(xid, branchId) == "CANCELLED") {
throw new IllegalStateException("事务已取消,禁止 Try");
}
2.4.2 时间戳机制
为事务记录时间戳:
- Cancel 更新状态时记录时间戳。
- Try 检查时间戳,若晚于 Cancel 时间,则失败。
2.4.3 分布式锁
使用分布式锁(如 Redis):
- Try 和 Cancel 操作竞争锁。
- Cancel 执行后,Try 无法获取锁,直接失败。
三、综合优化
3.1 Seata 配置
- 全局事务表 :利用
global_table
和branch_table
跟踪状态。 - 超时调整:延长 Try 超时时间,减少误判。
3.2 业务设计
- 幂等性:Try、Confirm、Cancel 接口均需幂等。
- 状态机:明确事务状态流转,避免时序混乱。
3.3 监控
部署 Seata Dashboard,实时监控分支事务状态,发现异常及时处理。
四、总结
通过 A、B、C 分支事务的时序分析:
- 空回滚:Cancel 在 Try 未执行时被调用,可通过状态记录解决。
- 悬挂:Cancel 先于 Try 执行,可通过前置检查和锁机制解决。