ITPUB

万亿级资金流转,TCC如何实现资金零差错?

前言

“用户支付了钱,订单却显示失败!”
“转账扣了A账户,B账户却没到账!”
在支付宝中,资金类操作必须实现零差错,否则分分钟引发客诉甚至法律风险!

需求:设计跨行转账服务,A银行扣款1万元 → B银行入账1万元,要求:

  • 资金零误差,任何异常都要有补偿机制

  • 高并发下性能稳定,避免数据库长事务锁

  • 支持跨网络、跨数据中心的调用

传统2PC(两阶段提交)在资金交易场景中的致命缺陷

缺陷维度
技术表现
资金场景影响案例
数据指标
同步阻塞
所有参与者锁定资源直至协议完成
大额转账时锁账户超30秒
吞吐量下降72% (TPS 1500→420)
协调者单点故障
协调者宕机导致全局事务悬挂
支付网关故障引发5000笔交易状态不确定
MTTR>15分钟,资损风险0.3%
数据不一致
阶段二部分参与者提交失败
跨行转账扣款成功但入账失败
对账误差率0.07%
网络分区耐受差
脑裂场景无法自动恢复
机房断网导致跨地域账户余额漂移
30分钟不可用,资损放大效应
协议僵局
失败回滚时参与者失联
商户结算因节点宕机冻结资金流
人工干预率18%

一、TCC核心思想:业务逻辑补偿

1. 三阶段操作解析

阶段
目标
关键动作
Try
资源预留
冻结A账户1万,B账户预增1万
Confirm
确认提交
实际扣减A账户,增加B账户
Cancel
回滚补偿
释放A账户冻结金额,撤销B预增

举个栗子🌰:

  • 正常流程:Try成功 → Confirm提交

  • 异常流程:Try失败 → Cancel回滚

  • 极端情况:Try成功但Confirm超时 → 定时任务重试

图片

二、代码实战:手撕TCC三阶段

1. Try阶段:资源冻结

// 账户服务接口  public interface AccountService {      @Transactional      boolean tryDeduct(String accountId, BigDecimal amount);  // 冻结资金  
    boolean confirmDeduct(String accountId, BigDecimal amount); // 实际扣款      boolean cancelDeduct(String accountId, BigDecimal amount);  // 释放冻结  }  
// A银行Try实现(伪代码)  public boolean tryDeduct(String accountId, BigDecimal amount) {      Account account = accountDao.selectForUpdate(accountId); // 加行锁      if (account.getBalance().compareTo(amount) >= 0) {          account.setFrozenAmount(account.getFrozenAmount().add(amount));          accountDao.update(account);          return true;      }      return false;  }  

2. Confirm阶段:实际扣款

public boolean confirmDeduct(String accountId, BigDecimal amount) {      Account account = accountDao.select(accountId);      account.setBalance(account.getBalance().subtract(amount));      account.setFrozenAmount(account.getFrozenAmount().subtract(amount));      accountDao.update(account);      return true;  }  

3. Cancel阶段:反向补偿

public boolean cancelDeduct(String accountId, BigDecimal amount) {      Account account = accountDao.select(accountId);      account.setFrozenAmount(account.getFrozenAmount().subtract(amount));      accountDao.update(account);      return true;  }  

关键设计:

  • 每个服务都要实现Try/Confirm/Cancel接口

  • 事务管理器(如Seata)记录操作日志,用于重试

三、TCC三大核心问题与解决方案

1. 空回滚问题

场景:Try未执行,但Cancel被调用(比如网络超时后触发回滚)
解法:

  • 在Cancel接口中检查Try是否执行

public boolean cancelDeduct(String accountId, BigDecimal amount) {      // 检查是否存在冻结记录      Account account = accountDao.select(accountId);      if (account.getFrozenAmount().compareTo(amount) < 0) {          log.warn("空回滚,直接返回");          return true;      }      // 正常回滚逻辑...  }  

2. 幂等性问题

场景:网络重试导致Confirm/Cancel被重复调用
解法:

  • 每个事务增加唯一ID,记录执行状态

// 事务状态表  class TransactionLog {      String txId;      int status; // 0-init, 1-try, 2-confirm, 3-cancel  }  
public boolean confirmDeduct(String txId, String accountId, BigDecimal amount) {      if (transactionLogDao.existsByTxIdAndStatus(txId, 2)) {          return true; // 已确认,直接返回      }      // 执行确认逻辑...  }  

3. 悬挂问题

场景:Cancel比Try先到达(Try超时后触发Cancel,但之后Try又执行了)
解法:

  • 在Try阶段检查是否存在已回滚记录

public boolean tryDeduct(String txId, String accountId, BigDecimal amount) {      if (transactionLogDao.existsByTxIdAndStatus(txId, 3)) {          log.error("事务已回滚,拒绝Try操作");          return false;      }      // 正常Try逻辑...  }  

四、TCC vs 其他方案:为什么资金场景必须用TCC?

方案
一致性
性能
适用场景
2PC
强一致
差(同步阻塞)
数据库层简单事务
Saga
最终一致
高(异步)
长流程、可补偿业务
TCC
最终一致
高(资源预留)
资金、库存等敏感操作

核心结论:

  • TCC通过业务层补偿,避免数据库长锁

  • 资金操作必须保留中间状态(冻结金额),不允许直接扣减

五、面试暴击指南

  1. 致命问题:

  • “TCC的Cancel操作失败怎么办?”

  • 答:“通过异步重试+告警人工介入,同时设计前期的资源预留足够覆盖逆操作”

  • 高阶话术:

    • “我们在支付系统中采用TCC+本地消息表混合方案,Confirm阶段失败时,用消息队列保证最终提交”

    直播预告
    6月19日,ITPUB携手行业专家围绕“ 大模型训练技术:原理与实践”这一主题开展线上直播分享,点击下方卡片预约直播:

    Image