WEBKT

TCC事务Cancel幂等失效:利用状态机模式防止资金双倍回滚的设计方案

41 0 0 0

这是一个非常经典且致命的分布式事务问题。在TCC(Try-Confirm-Cancel)模型中,Try阶段通常会冻结资源(比如扣减预存款),而Cancel阶段负责解冻或回滚。如果Cancel阶段因为网络抖动重试,而业务上没有做好幂等性保护,确实会导致资金被重复回滚,即“资金双倍回滚”的资损事故。

解决这个问题的核心在于将幂等性下沉到资源表的状态机设计中,而不是单纯依赖Try阶段的资源冻结记录。

以下是如何设计一张具备防重入幂等能力的资源表(以资金流水表为例):

1. 核心状态机流转设计

我们需要在资源表中引入明确的状态字段,利用状态机的不可逆性来规避重复Cancel。

关键字段设计:

  • tx_id (事务ID): 全局唯一,用于关联Try/Confirm/Cancel。
  • amount (金额): 冻结或扣减的数值。
  • status (状态): 核心状态字段,通常包含:
    • INIT (初始)
    • TRYING (冻结成功/资源锁定)
    • CONFIRMED (提交成功)
    • CANCELED (回滚成功)
  • version (乐观锁版本号): 用于处理并发更新。

2. 状态流转与防重逻辑

Try 阶段:

  1. 插入一条记录,状态设为 TRYING
  2. 扣减账户余额(例如:UPDATE account SET balance = balance - 100 WHERE id = user_id)。

Cancel 阶段(防重核心):
当Cancel请求到达时,执行以下原子操作:

UPDATE resource_table 
SET status = 'CANCELED', version = version + 1
WHERE tx_id = 'global_tx_id_123' 
  AND status = 'TRYING'  -- 关键点1:只能从TRYING流转到CANCELED
  AND version = current_version; -- 关键点2:乐观锁防并发

逻辑分析:

  1. 幂等性: 如果Cancel因为网络抖动重试了,第一次Cancel成功后,status 变成了 CANCELED。当第二次Cancel请求到达时,SQL中的 AND status = 'TRYING' 条件不满足,affected_rows 为0,执行无效操作,从而避免了重复回滚。
  2. 防资损: 这种设计利用了状态机的不可逆性(TRYING -> CANCELED),如果业务逻辑中还存在 CONFIRMED 状态,Cancel 操作通常还应该加上 AND status != 'CONFIRMED',防止在Confirm之后误执行Cancel。

3. 异常情况处理

如果Try阶段成功但本地事务回滚(未插入状态记录),或者Cancel请求在Try插入记录之前到达:

  • Try前置检查: Cancel执行前先查询是否存在该 tx_id 的记录。如果不存在,说明Try没执行过,直接返回成功(空回滚)。
  • 空回滚防御: 如果Try因为异常没执行,但Cancel执行了,会导致数据不一致。解决方法是在Cancel时,如果发现没有对应的Try记录,需要插入一条逆向的补偿记录(或者仅记录日志并告警,视业务容忍度而定)。

总结

通过状态机 + 乐观锁 + 预期状态判断,我们将幂等性问题转化为了数据库字段的约束问题。这比在代码层做 if (hasCanceled) return 更加健壮,因为数据库约束是原子的,是最后一道防线。

码农架构师 TCC事务幂等性设计分布式事务

评论点评