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 阶段:
- 插入一条记录,状态设为
TRYING。 - 扣减账户余额(例如:
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:乐观锁防并发
逻辑分析:
- 幂等性: 如果Cancel因为网络抖动重试了,第一次Cancel成功后,
status变成了CANCELED。当第二次Cancel请求到达时,SQL中的AND status = 'TRYING'条件不满足,affected_rows为0,执行无效操作,从而避免了重复回滚。 - 防资损: 这种设计利用了状态机的不可逆性(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 更加健壮,因为数据库约束是原子的,是最后一道防线。