【架构实战】分布式事务TCC模式:两阶段提交的工程艺术

一、我与TCC的"相爱相杀"

第一次接触TCC模式,是在一个支付系统中。

当时的场景是这样的:用户发起一笔转账,从账户A转100块到账户B。传统方式:

复制代码
账户A:balance -= 100
账户B:balance += 100

这两个操作在不同服务里,网络抖动、服务器宕机......各种情况都可能导致数据不一致。

我当时的方案是:写一条"转账记录"到消息表,用定时任务补偿。这个方案勉强能用,但:

  • 定时任务延迟大,用户可能等很久
  • 补偿逻辑复杂,容易出错
  • 很难保证"恰好一次"

后来我接触到了TCC模式,才发现这才是正确的打开方式。但TCC的概念看起来简单,真正落地时遇到的坑,让我差点放弃:

  • Try阶段的资源冻结时机不对
  • Confirm和Cancel的幂等没做好
  • 空回滚问题困扰了很久
  • 悬挂问题差点让我多扣了用户的钱

今天这篇文章,是我踩坑踩出来的经验总结,希望能帮你少走弯路。


二、TCC原理:理解两阶段提交

2.1 TCC的三阶段

TCC = Try + Confirm + Cancel,这三个阶段对应分布式事务的生命周期:

复制代码
TCC事务生命周期:

┌─────────────────────────────────────────────────────────┐
│                                                          │
│  ┌─────────────────────────────────────────────────┐    │
│  │              第一阶段:Try(预留资源)               │    │
│  │                                                   │    │
│  │  账户A:冻结100元(balance_frozen += 100)       │    │
│  │  账户B:预增加100元(balance_pending += 100)    │    │
│  │                                                   │    │
│  │  特点:                                          │    │
│  │  - 只预留资源,不真正扣减                         │    │
│  │  - 失败了也不会有实际损失                        │    │
│  │  - 所有参与者的Try都要成功                        │    │
│  └─────────────────────────────────────────────────┘    │
│                         ↓                               │
│  ┌─────────────────────────────────────────────────┐    │
│  │              第二阶段:Confirm(确认执行)            │    │
│  │                                                   │    │
│  │  账户A:真正扣减(balance -= 100, frozen -= 100)  │    │
│  │  账户B:真正增加(balance += 100, pending -= 100)│    │
│  │                                                   │    │
│  │  特点:                                          │    │
│  │  - 使用Try阶段预留的资源                         │    │
│  │  - 必须成功,不能失败                            │    │
│  └─────────────────────────────────────────────────┘    │
│                         ↓                               │
│              ┌───────────────────────┐                  │
│              │  Confirm失败/超时     │                  │
│              └───────────┬───────────┘                  │
│                          ↓                              │
│  ┌─────────────────────────────────────────────────┐    │
│  │              第三阶段:Cancel(回滚)               │    │
│  │                                                   │    │
│  │  账户A:解冻(frozen -= 100)                    │    │
│  │  账户B:回退(pending -= 100)                   │    │
│  │                                                   │    │
│  │  特点:                                          │    │
│  │  - 释放Try阶段预留的资源                         │    │
│  │  - Confirm失败也会触发Cancel                     │    │
│  └─────────────────────────────────────────────────┘    │
│                                                          │
└─────────────────────────────────────────────────────────┘

2.2 TCC与2PC的区别

维度 2PC(两阶段提交) TCC
协调者 数据库协调者 应用协调者(TC)
执行位置 数据库层面 应用层面
资源锁定 锁定数据行 预留/冻结资源
阻塞 锁定期间阻塞 不阻塞其他事务
回滚 数据库回滚 业务补偿
性能 较差 较好

2.3 为什么需要TCC

复制代码
场景:转账100元,账户A和账户B在不同的服务

不用TCC:
  账户A扣100元成功
  账户B加100元失败(网络超时)
  结果:钱丢了!

用TCC:
  Try阶段:账户A冻结100元,账户B预增加100元(都成功)
  Confirm阶段:账户A真正扣,账户B真正加(都成功)
  
  或者:
  Try阶段:账户A冻结100元,账户B预增加100元(账户B超时失败)
  Cancel阶段:账户A解冻100元(回滚)

三、TCC实现:代码层面的细节

3.1 TCC接口定义

java 复制代码
// TCC接口需要标注@LocalTCC
@LocalTCC
public interface TransferTccService {
    
    /**
     * Try阶段:预留资源
     * @param actionContext 事务上下文,Seata自动传递
     * @param fromAccount 付款账户
     * @param toAccount 收款账户
     * @param amount 金额
     */
    @TwoPhaseBusinessAction(
        name = "transferAction",
        commitMethod = "confirm",
        rollbackMethod = "cancel",
        tryTimeout = 30000  // Try超时时间(毫秒)
    )
    boolean tryTransfer(
        BusinessActionContext actionContext,
        @BusinessActionContextParameter(paramName = "fromAccount") String fromAccount,
        @BusinessActionContextParameter(paramName = "toAccount") String toAccount,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    );
    
    /**
     * Confirm阶段:确认执行
     * 必须幂等,失败会重试
     */
    boolean confirm(BusinessActionContext actionContext);
    
    /**
     * Cancel阶段:回滚
     * 必须幂等,失败会重试
     */
    boolean cancel(BusinessActionContext actionContext);
}

3.2 业务实现:Try阶段

java 复制代码
@Service
public class TransferTccServiceImpl implements TransferTccService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Autowired
    private TransferLogMapper transferLogMapper;
    
    @Override
    @Transactional
    public boolean tryTransfer(
            BusinessActionContext context,
            String fromAccount,
            String toAccount,
            BigDecimal amount) {
        
        // 获取全局事务ID(用于幂等)
        String xid = context.getXid();
        String fromKey = fromAccount + "_" + xid;
        
        // ========== 1. 检查幂等 ==========
        // 如果已经有转账记录,说明是重复执行
        if (transferLogMapper.existsByXidAndAction(fromKey, "try")) {
            log.info("Try阶段已执行过,幂等跳过: xid={}", xid);
            return true;
        }
        
        // ========== 2. 检查账户A余额 ==========
        Account fromAccountEntity = accountMapper.selectByAccountNo(fromAccount);
        if (fromAccountEntity == null) {
            throw new RuntimeException("付款账户不存在");
        }
        
        // ========== 3. 检查余额是否足够 ==========
        if (fromAccountEntity.getAvailableBalance().compareTo(amount) < 0) {
            throw new RuntimeException("余额不足");
        }
        
        // ========== 4. 冻结金额 ==========
        // 冻结金额 = 原冻结 + 本次转账金额
        BigDecimal newFrozenBalance = 
            fromAccountEntity.getFrozenBalance().add(amount);
        accountMapper.updateFrozenBalance(fromAccount, newFrozenBalance);
        
        // ========== 5. 记录转账日志(幂等标记) ==========
        TransferLog log = new TransferLog();
        log.setXid(xid);
        log.setActionKey(fromKey);
        log.setAction("try");
        log.setFromAccount(fromAccount);
        log.setToAccount(toAccount);
        log.setAmount(amount);
        log.setStatus("PENDING");  // 待确认
        log.setCreateTime(new Date());
        transferLogMapper.insert(log);
        
        log.info("Try阶段完成:冻结{}元 from={}, xid={}", amount, fromAccount, xid);
        return true;
    }
}

3.3 业务实现:Confirm阶段

java 复制代码
@Override
public boolean confirm(BusinessActionContext context) {
    String xid = context.getXid();
    String fromAccount = (String) context.getActionContext("fromAccount");
    String toAccount = (String) context.getActionContext("toAccount");
    BigDecimal amount = new BigDecimal(
        context.getActionContext("amount").toString()
    );
    
    String fromKey = fromAccount + "_" + xid;
    
    // ========== 1. 检查幂等 ==========
    // 如果Confirm已执行,跳过
    if (transferLogMapper.existsByXidAndAction(fromKey, "confirm")) {
        log.info("Confirm阶段已执行过,幂等跳过: xid={}", xid);
        return true;
    }
    
    try {
        // ========== 2. 从冻结转为真正扣减 ==========
        Account fromEntity = accountMapper.selectByAccountNo(fromAccount);
        Account toEntity = accountMapper.selectByAccountNo(toAccount);
        
        // 扣减可用余额(恢复冻结,转入冻结)
        BigDecimal newFrozenBalance = fromEntity.getFrozenBalance().subtract(amount);
        fromEntity.setFrozenBalance(newFrozenBalance);
        fromEntity.setAvailableBalance(
            fromEntity.getAvailableBalance().subtract(amount)
        );
        accountMapper.updateById(fromEntity);
        
        // 增加目标账户余额
        toEntity.setAvailableBalance(
            toEntity.getAvailableBalance().add(amount)
        );
        accountMapper.updateById(toEntity);
        
        // ========== 3. 记录Confirm日志 ==========
        TransferLog log = new TransferLog();
        log.setXid(xid);
        log.setActionKey(fromKey);
        log.setAction("confirm");
        log.setFromAccount(fromAccount);
        log.setToAccount(toAccount);
        log.setAmount(amount);
        log.setStatus("CONFIRMED");
        log.setCreateTime(new Date());
        transferLogMapper.insert(log);
        
        log.info("Confirm阶段完成:真正扣减{}元 from={}, xid={}", amount, fromAccount, xid);
        return true;
        
    } catch (Exception e) {
        // Confirm失败会重试,抛出异常
        log.error("Confirm阶段失败: xid={}", xid, e);
        throw new RuntimeException("Confirm失败", e);
    }
}

3.4 业务实现:Cancel阶段

java 复制代码
@Override
public boolean cancel(BusinessActionContext context) {
    String xid = context.getXid();
    String fromAccount = (String) context.getActionContext("fromAccount");
    BigDecimal amount = new BigDecimal(
        context.getActionContext("amount").toString()
    );
    
    String fromKey = fromAccount + "_" + xid;
    
    // ========== 1. 检查幂等 ==========
    if (transferLogMapper.existsByXidAndAction(fromKey, "cancel")) {
        log.info("Cancel阶段已执行过,幂等跳过: xid={}", xid);
        return true;
    }
    
    // ========== 2. 解冻金额 ==========
    Account fromEntity = accountMapper.selectByAccountNo(fromAccount);
    
    if (fromEntity == null) {
        // 账户不存在,说明是空回滚
        log.warn("Cancel:账户{}不存在,可能是空回滚, xid={}", fromAccount, xid);
        // 空回滚也需要记录日志
        recordCancelLog(xid, fromKey, fromAccount, amount, "ACCOUNT_NOT_EXISTS");
        return true;
    }
    
    // ========== 3. 检查冻结金额 ==========
    // 如果冻结金额为0或负数,可能是Try没执行(空回滚)或者已经Cancel过了
    if (fromEntity.getFrozenBalance().compareTo(amount) < 0) {
        // 可能是空回滚:Try没执行或已取消
        log.warn("Cancel:冻结金额{} < {}, 可能是空回滚, xid={}", 
            fromEntity.getFrozenBalance(), amount, xid);
        recordCancelLog(xid, fromKey, fromAccount, amount, "INSUFFICIENT_FROZEN");
        return true;  // 空回滚返回成功
    }
    
    // ========== 4. 执行解冻 ==========
    BigDecimal newFrozenBalance = fromEntity.getFrozenBalance().subtract(amount);
    fromEntity.setFrozenBalance(newFrozenBalance);
    // 余额不变,只是释放冻结
    accountMapper.updateById(fromEntity);
    
    // ========== 5. 记录Cancel日志 ==========
    recordCancelLog(xid, fromKey, fromAccount, amount, "SUCCESS");
    
    log.info("Cancel阶段完成:解冻{}元 from={}, xid={}", amount, fromAccount, xid);
    return true;
}

private void recordCancelLog(String xid, String fromKey, String fromAccount, 
                            BigDecimal amount, String reason) {
    TransferLog log = new TransferLog();
    log.setXid(xid);
    log.setActionKey(fromKey);
    log.setAction("cancel");
    log.setFromAccount(fromAccount);
    log.setAmount(amount);
    log.setStatus("CANCELLED");
    log.setRemark("Cancel原因: " + reason);
    log.setCreateTime(new Date());
    transferLogMapper.insert(log);
}

四、TCC三剑客:幂等、空回滚、悬挂

4.1 幂等性:保证TCC操作不重复

为什么需要幂等?

复制代码
Confirm和Cancel可能会执行多次:
- 网络抖动导致超时
- TC(事务协调器)重试
- 服务重启后恢复

所以每个阶段都必须幂等!
java 复制代码
// 幂等实现方式1:日志表(最常用)
@Service
public class IdempotentService {
    
    @Autowired
    private TccActionLogMapper logMapper;
    
    public boolean tryAction(String xid, String action) {
        // 查询是否执行过
        if (logMapper.exists(xid, action)) {
            return true;  // 已执行,幂等跳过
        }
        
        // 执行业务逻辑
        doBusiness();
        
        // 记录执行日志
        logMapper.insert(xid, action, "SUCCESS");
        return true;
    }
}

// 幂等实现方式2:状态机
// 使用状态字段控制
// Try: PENDING -> CONFIRMING
// Cancel: PENDING -> CANCELLING
// CONFIRMING -> CONFIRMED
// CANCELLING -> CANCELLED

4.2 空回滚:Try没执行,Cancel执行了

为什么会出现?

复制代码
Try超时 → TC认为Try失败 → 触发Cancel
实际上Try可能成功了!
java 复制代码
@Override
public boolean cancel(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 检查Try是否执行过
    boolean tryExecuted = tryLogMapper.exists(xid);
    
    if (!tryExecuted) {
        // Try没执行,这是空回滚
        // 空回滚不需要解冻,直接记录即可
        log.warn("空回滚:xid={}", xid);
        
        // 记录空回滚日志
        logMapper.insertEmptyCancel(xid, context);
        return true;
    }
    
    // Try执行过了,正常回滚
    return doNormalCancel(context);
}

4.3 悬挂:Cancel比Try先执行

为什么会出现?

复制代码
Cancel超时 → TC认为Cancel失败,重试Cancel
实际上Cancel可能成功了,Try后执行却因为"已取消"而失败
java 复制代码
@Override
public boolean tryAction(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 检查Cancel是否执行过(防止悬挂)
    boolean cancelExecuted = cancelLogMapper.exists(xid, "cancel");
    
    if (cancelExecuted) {
        // Cancel已执行,这是悬挂
        // Try不能执行,否则会导致数据错误
        log.warn("悬挂:Cancel已执行,Try跳过: xid={}", xid);
        return true;  // 返回成功但不做任何操作
    }
    
    // Cancel没执行,正常执行Try
    return doNormalTry(context);
}

4.4 三剑客的协同

java 复制代码
// 完整的三剑客处理
@Override
public boolean tryAction(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 1. 幂等检查(防止重复Try)
    if (actionLogMapper.exists(xid, "try")) {
        return true;
    }
    
    // 2. 悬挂检查(Cancel执行过就不能Try)
    if (actionLogMapper.exists(xid, "cancel")) {
        return true;
    }
    
    // 3. 执行Try
    doTry();
    
    // 4. 记录日志
    actionLogMapper.insert(xid, "try", "SUCCESS");
    return true;
}

@Override
public boolean confirm(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 1. 幂等检查
    if (actionLogMapper.exists(xid, "confirm")) {
        return true;
    }
    
    // 2. 执行Confirm
    doConfirm();
    
    // 3. 记录日志
    actionLogMapper.insert(xid, "confirm", "SUCCESS");
    return true;
}

@Override
public boolean cancel(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 1. 幂等检查
    if (actionLogMapper.exists(xid, "cancel")) {
        return true;
    }
    
    // 2. 空回滚检查(Try没执行)
    if (!actionLogMapper.exists(xid, "try")) {
        // 空回滚,记录日志即可
        actionLogMapper.insert(xid, "cancel", "EMPTY_CANCEL");
        return true;
    }
    
    // 3. 执行Cancel
    doCancel();
    
    // 4. 记录日志
    actionLogMapper.insert(xid, "cancel", "SUCCESS");
    return true;
}

五、TCC实战:扣款与退款场景

5.1 扣款场景

java 复制代码
@LocalTCC
public interface DeductionTccService {
    
    @TwoPhaseBusinessAction(
        name = "deductAction",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryDeduct(
        BusinessActionContext context,
        @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount,
        @BusinessActionContextParameter(paramName = "orderId") String orderId
    );
    
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

@Service
public class DeductionTccServiceImpl implements DeductionTccService {
    
    @Override
    public boolean tryDeduct(BusinessActionContext context,
                             String accountNo, BigDecimal amount, String orderId) {
        // 1. 幂等检查
        if (deductionLogMapper.existsByOrderId(orderId, "try")) {
            return true;
        }
        
        // 2. 检查余额
        Account account = accountMapper.selectByAccountNo(accountNo);
        if (account.getAvailableBalance().compareTo(amount) < 0) {
            throw new RuntimeException("余额不足,无法扣款");
        }
        
        // 3. 冻结金额
        accountMapper.freeze(accountNo, amount);
        
        // 4. 记录日志
        deductionLogMapper.insert(orderId, accountNo, amount, "TRY");
        
        return true;
    }
    
    @Override
    public boolean confirm(BusinessActionContext context) {
        String orderId = (String) context.getActionContext("orderId");
        
        // 幂等检查
        if (deductionLogMapper.existsByOrderId(orderId, "confirm")) {
            return true;
        }
        
        // 真正扣减可用余额
        String accountNo = (String) context.getActionContext("accountNo");
        BigDecimal amount = new BigDecimal(
            context.getActionContext("amount").toString()
        );
        
        accountMapper.deductBalance(accountNo, amount);
        
        // 更新日志
        deductionLogMapper.updateStatus(orderId, "confirm", "CONFIRMED");
        
        return true;
    }
    
    @Override
    public boolean cancel(BusinessActionContext context) {
        String orderId = (String) context.getActionContext("orderId");
        
        // 幂等检查
        if (deductionLogMapper.existsByOrderId(orderId, "cancel")) {
            return true;
        }
        
        // 空回滚检查
        if (!deductionLogMapper.existsByOrderId(orderId, "try")) {
            // 空回滚
            deductionLogMapper.insert(orderId, 
                (String) context.getActionContext("accountNo"),
                new BigDecimal(context.getActionContext("amount").toString()),
                "CANCEL_EMPTY"
            );
            return true;
        }
        
        // 解冻金额
        String accountNo = (String) context.getActionContext("accountNo");
        BigDecimal amount = new BigDecimal(
            context.getActionContext("amount").toString()
        );
        
        accountMapper.unfreeze(accountNo, amount);
        
        // 更新日志
        deductionLogMapper.updateStatus(orderId, "cancel", "CANCELLED");
        
        return true;
    }
}

5.2 退款场景

java 复制代码
@LocalTCC
public interface RefundTccService {
    
    @TwoPhaseBusinessAction(
        name = "refundAction",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryRefund(
        BusinessActionContext context,
        @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount,
        @BusinessActionContextParameter(paramName = "refundId") String refundId
    );
    
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

@Service
public class RefundTccServiceImpl implements RefundTccService {
    
    @Override
    public boolean tryRefund(BusinessActionContext context,
                            String accountNo, BigDecimal amount, String refundId) {
        // 幂等检查
        if (refundLogMapper.existsByRefundId(refundId, "try")) {
            return true;
        }
        
        // 记录退款日志(待确认)
        RefundLog log = new RefundLog();
        log.setRefundId(refundId);
        log.setAccountNo(accountNo);
        log.setAmount(amount);
        log.setStatus("PENDING");
        log.setCreateTime(new Date());
        refundLogMapper.insert(log);
        
        return true;
    }
    
    @Override
    public boolean confirm(BusinessActionContext context) {
        String refundId = (String) context.getActionContext("refundId");
        
        if (refundLogMapper.existsByRefundId(refundId, "confirm")) {
            return true;
        }
        
        // 增加账户余额
        String accountNo = (String) context.getActionContext("accountNo");
        BigDecimal amount = new BigDecimal(
            context.getActionContext("amount").toString()
        );
        
        accountMapper.addBalance(accountNo, amount);
        
        // 更新日志
        refundLogMapper.updateStatus(refundId, "CONFIRMED");
        
        return true;
    }
    
    @Override
    public boolean cancel(BusinessActionContext context) {
        String refundId = (String) context.getActionContext("refundId");
        
        if (refundLogMapper.existsByRefundId(refundId, "cancel")) {
            return true;
        }
        
        // 更新日志(退款取消)
        refundLogMapper.updateStatus(refundId, "CANCELLED");
        
        return true;
    }
}

六、TCC与Seata集成

6.1 Seata TCC配置

yaml 复制代码
# Seata TCC模式配置
seata:
  enabled: true
  application-id: payment-service
  tx-service-group: payment-tx-group
  
  # TCC模式配置
  tm:
    rollback-retry-timeout-unlock-enable: true  # 回滚重试超时解锁
  
  rm:
    # TCC模式资源自动注册
    enable-auto-data-source-proxy: false  # TCC不需要自动代理

6.2 资源自动注册

java 复制代码
// Seata TCC模式下,资源会自动注册
// 但需要配置TCC接口的实现

@LocalTCC
public interface OrderTccService {
    
    @TwoPhaseBusinessAction(
        name = "orderAction",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryCreateOrder(
        BusinessActionContext context,
        @BusinessActionContextParameter(paramName = "orderId") String orderId,
        @BusinessActionContextParameter(paramName = "userId") String userId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    );
    
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

6.3 多服务TCC调用

java 复制代码
// 跨多个服务的TCC事务
@Service
public class UnifiedPaymentService {
    
    @Autowired
    private DeductionTccService deductionTccService;
    
    @Autowired
    private RefundTccService refundTccService;
    
    @GlobalTransactional(name = "unified-payment", rollbackFor = Exception.class)
    public void unifiedPayment(String userId, BigDecimal amount, 
                              String deductionOrderId, String refundOrderId) {
        
        // 1. 扣款(TCC Try)
        deductionTccService.tryDeduct(null, userId, amount, deductionOrderId);
        
        // 2. 退款记录(TCC Try)
        refundTccService.tryRefund(null, userId, amount, refundOrderId);
        
        // 如果这里抛出异常:
        // - deductionTccService.cancel() 会被调用
        // - refundTccService.cancel() 会被调用
        // 这就是TCC的自动回滚机制
    }
}

七、TCC性能优化

7.1 并行Try阶段

java 复制代码
// 如果多个TCC调用之间没有依赖,可以并行执行
@Service
public class ParallelTccService {
    
    @GlobalTransactional
    public void parallelExecute() {
        // 使用CompletableFuture并行执行
        CompletableFuture.allOf(
            CompletableFuture.runAsync(() -> tccService1.tryAction(...)),
            CompletableFuture.runAsync(() -> tccService2.tryAction(...)),
            CompletableFuture.runAsync(() -> tccService3.tryAction(...))
        ).join();  // 等待所有完成
        
        // 任一失败会触发全局回滚
    }
}

7.2 异步Confirm

java 复制代码
// Seata支持异步Confirm,提高性能
@GlobalTransactional(asyncConfirmation = true)
public void asyncConfirmPayment() {
    // Try阶段同步执行
    deductionTccService.tryDeduct(...);
    
    // Confirm阶段异步执行
    // 可以在Seata控制台查看异步任务状态
}

7.3 批量处理

java 复制代码
// 批量TCC操作,减少网络开销
@LocalTCC
public interface BatchTccService {
    
    @TwoPhaseBusinessAction(name = "batchAction", ...)
    boolean tryBatch(BusinessActionContext context,
                     @BusinessActionContextParameter(paramName = "items") List<BatchItem> items);
    
    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

八、TCC监控与问题排查

8.1 TCC事务监控

yaml 复制代码
# Seata TCC监控指标
management:
  metrics:
    export:
      prometheus:
        enabled: true
        
# 关键指标:
# - seata_tcc_transaction_count: TCC事务总数
# - seata_tcc_transaction_success: TCC事务成功数
# - seata_tcc_transaction_fail: TCC事务失败数

8.2 常见问题排查

bash 复制代码
# TCC问题排查指南

问题1:Try执行成功,Confirm没执行
原因:Seata Server异常或网络问题
排查:
1. 检查Seata Server状态
2. 查看Seata Server日志
3. 检查网络连通性

问题2:Cancel执行多次
原因:Confirm失败导致重试Cancel
排查:
1. 查看Confirm失败原因
2. 确保Confirm幂等

问题3:空回滚
原因:Try超时或网络问题
排查:
1. 正常现象,说明Try确实没执行
2. 检查Try执行环境和日志

九、踩坑实录

坑1:Try阶段的余额检查时机

某次实现扣款TCC,Try阶段检查余额时扣款100元成功,但Confirm阶段因为数据库问题失败了。

结果:TC触发了Cancel回滚,用户的钱被解冻了。但实际上钱应该被扣掉!

教训:Try阶段只是"告诉我要扣",真正扣钱应该在Confirm。如果Try阶段就真的扣了钱,Confirm失败就会导致问题。

坑2:Cancel时的余额检查

Cancel时检查冻结余额,如果冻结余额不足就跳过解冻。但实际上用户的余额可能已经被其他事务处理了。

教训:Cancel幂等处理,空回滚就记录日志,不要根据余额判断是否执行。

坑3:TCC接口参数不能为null

TCC接口中,BusinessActionContextParameter的参数不能为null,否则序列化会失败。

教训:确保所有@BusinessActionContextParameter标注的参数都有值。


十、总结

TCC是分布式事务的重要模式:

  • Try:预留资源,不真正扣减
  • Confirm:确认执行,使用预留资源
  • Cancel:回滚,释放预留资源
  • 幂等:每个阶段必须幂等
  • 空回滚:Try没执行时Cancel的处理
  • 悬挂:Cancel比Try先执行的防护

最佳实践:

  1. Try阶段只预留资源,不真正执行
  2. Confirm/Cancel必须幂等
  3. 处理空回滚和悬挂
  4. 使用日志表记录状态
  5. 合理设置Try超时时间
  6. 监控TCC事务状态

血的教训:

TCC模式需要业务代码实现补偿逻辑,这意味着业务侵入性较强。如果业务逻辑简单,用AT模式更合适;如果业务复杂、对性能要求高,TCC是更好的选择。

思考题: 你们项目有没有用过TCC模式?Try阶段是真正扣减还是只是冻结?Confirm失败了怎么办?


个人观点,仅供参考

相关推荐
大江东去浪淘尽千古风流人物1 小时前
【Kimera】MIT SPARK 实时度量-语义 SLAM 全栈解析:VIO + 鲁棒 PGO + 语义网格四模块架构与 EuRoC 实测深度剖析
大数据·架构·spark
GIS数据转换器1 小时前
蓄能电力大数据监管平台
大数据·人工智能·分布式·数据挖掘·数据分析·智慧城市
大江东去浪淘尽千古风流人物1 小时前
【Kimera-VIO】MIT SPARK 实时度量-语义 VIO/SLAM:六模块并行架构与智能因子图优化深度解析
大数据·架构·spark
大江东去浪淘尽千古风流人物1 小时前
【Kimera-Semantics】实时三维语义重建深度解析:Fast/Merged 双路积分、对数概率体素 Bayesian 融合与 ROS 全链路实现
大数据·架构·spark
zhangzeyuaaa1 小时前
Kafka 核心原理超通俗详解|Offset、消费组、分区、持久化一次讲透
分布式·kafka
@不误正业1 小时前
多Agent协作框架深度实战-从ReAct到Plan-and-Execute全架构演进
前端·react.js·架构·agent
小谢小哥1 小时前
59-消息推送系统详解
java·后端·架构
隔壁阿布都1 小时前
Kafka `acks` 参数取值全解
分布式·kafka
卷毛迷你猪1 小时前
小肥柴的Hadoop之旅 快速实验篇(0-1)虚拟机模拟完全分布式环境搭建
大数据·hadoop·分布式