WEBKT

分布式事务设计:如何通过补充字段解决Try空回滚与Confirm悬挂问题

30 0 0 0

在设计分布式事务或涉及Try/Confirm/Cancel流程的资源表时,除了基础的 status(状态)和 version(乐观锁版本号)字段外,要处理你提到的空回滚(Try执行了但没记录)和悬挂(Confirm执行了但Cancel又进来)这两个棘手问题,通常需要引入一套更完整的防悬挂和空回滚补偿机制

以下是建议补充的关键字段和设计机制:

1. 核心补充字段

为了唯一标识一笔业务,防止重复执行或遗漏执行,必须增加一个全局唯一的业务标识ID:

  • biz_id (业务流水号/全局事务ID):
    • 作用: 这是最关键的字段。Try、Confirm、Cancel三个阶段的操作必须关联同一个 biz_id
    • 解决场景:
      • 空回滚: 当Cancel阶段来临时,如果根据 biz_id 查询不到任何记录,说明Try阶段根本没有执行(或者数据被异常删除)。此时,Cancel操作不应该抛出异常,而应该直接返回成功,或者插入一条“虚拟”的空回滚记录以标记结束。
      • 悬挂: 当Try阶段来临时,如果发现该 biz_id 已经存在Confirm或Cancel的记录,说明这是一个过期的请求(网络延迟导致Try晚到了),此时Try操作必须拒绝执行

2. 针对“悬挂场景”的额外机制

悬挂场景的核心是:Confirm/Cancel 先执行,Try 后执行。这会导致 Try 阶段的数据被错误创建,无法被后续的 Cancel 清理。

除了 biz_id 的校验,通常还需要以下两种策略之一:

  • 策略 A:状态机前置检查 (利用 status 字段)

    • 在 Try 阶段执行前,必须先查询 status
    • 如果 status 已经是 CONFIRMEDCANCELED,说明事务已经结束,Try 阶段必须直接返回成功(幂等),绝对不能插入新的资源记录。
    • 这要求 status 的流转是不可逆的,且 Try 阶段必须先查后写。
  • 策略 B:插入“悬挂保护表”或利用“预留记录”

    • 字段 gmt_modified (修改时间): 配合逻辑判断。如果 Try 阶段发现记录存在,但修改时间晚于当前时间戳(或者对比业务逻辑时间),说明可能是悬挂。
    • 机制: 在 Try 阶段插入记录时,不仅仅插入资源表,还需要在一张专门的事务控制表中记录 biz_idtry_executed_at 时间戳。
    • Confirm/Cancel 执行时,检查该时间戳。如果 Try 的执行时间晚于 Confirm/Cancel 的执行时间,则判定为悬挂,Try 必须回滚自己插入的数据。

3. 针对“空回滚场景”的机制

空回滚的核心是:Cancel 先执行,Try 后执行。此时资源表没有数据,Cancel 如果直接报错,会导致事务流程中断。

  • 机制:
    • Cancel 阶段根据 biz_id 查询资源表。
    • 如果查不到记录:
      • 方案 1 (推荐): 直接返回成功,什么都不做。因为 Try 还没执行,后续 Try 执行时会看到 Cancel 已经结束(通过状态机),从而放弃执行。
      • 方案 2: 插入一条特殊的“空回滚记录”,标记该事务已取消。Try 阶段执行时,如果发现这条记录,则执行回滚逻辑并删除它。

总结:推荐的表结构设计

为了保证事务最终一致性,资源表建议至少包含以下字段:

字段名 类型 说明
id BigInt 主键
biz_id Varchar 核心:全局唯一ID,用于关联所有阶段
status TinyInt 状态:0-Trying, 1-Confirmed, 2-Cancelled, 3-EmptyRollback
version Int 乐观锁:防止并发修改导致的数据不一致
resource_data Text/Json 业务资源数据
gmt_create Datetime 创建时间(用于判断悬挂:Try不能晚于Confirm太久)
gmt_modified Datetime 修改时间

逻辑伪代码示例 (Try 阶段防悬挂):

// Try 阶段逻辑
public void tryTransaction(BizDTO dto) {
    // 1. 检查是否已经存在该业务ID的记录
    Resource record = resourceDao.selectByBizId(dto.getBizId());
    
    if (record != null) {
        // 2. 如果存在,检查状态是否为已结束 (Confirmed/Cancelled)
        if (record.getStatus() == CONFIRMED || record.getStatus() == CANCELLED) {
            // 说明发生了悬挂:Confirm/Cancel 先执行了
            // 此时 Try 必须放弃,直接返回,保证幂等性
            return; 
        }
        // 如果状态是 Trying,说明是重试,继续执行即可
    }

    // 3. 执行 Try 业务逻辑,插入资源记录
    // 注意:这里通常会插入一条 status=Trying 的记录
    resourceDao.insert(new Resource(dto.getBizId(), TRYING, ...));
}

逻辑伪代码示例 (Cancel 阶段防空回滚):

// Cancel 阶段逻辑
public void cancelTransaction(BizDTO dto) {
    // 1. 根据 BizId 查询
    Resource record = resourceDao.selectByBizId(dto.getBizId());
    
    // 2. 如果查不到记录(空回滚)
    if (record == null) {
        // 直接返回成功,或者插入一条空回滚标记
        // 不抛异常,避免阻断事务框架的重试机制
        return; 
    }

    // 3. 如果记录存在,使用乐观锁更新
    int rows = resourceDao.updateStatusByIdAndVersion(
        dto.getBizId(), CANCELLED, record.getVersion()
    );
    
    if (rows == 0) {
        throw new ConcurrencyException("并发修改,请重试");
    }
    
    // 4. 执行具体的资源释放逻辑
    releaseResource(record);
}

通过 biz_id 结合 status 状态机和 version 乐观锁,可以完美覆盖空回滚和悬挂这两个极端场景,确保数据的最终一致性。

架构师老王 分布式事务TCC模式数据一致性

评论点评