万亿级资金流转,TCC如何实现资金零差错?
前言
“用户支付了钱,订单却显示失败!”
“转账扣了A账户,B账户却没到账!”
在支付宝中,资金类操作必须实现零差错,否则分分钟引发客诉甚至法律风险!
需求:设计跨行转账服务,A银行扣款1万元 → B银行入账1万元,要求:
资金零误差,任何异常都要有补偿机制
高并发下性能稳定,避免数据库长事务锁
支持跨网络、跨数据中心的调用
传统2PC(两阶段提交)在资金交易场景中的致命缺陷
| 同步阻塞 | |||
| 协调者单点故障 | |||
| 数据不一致 | |||
| 网络分区耐受差 | |||
| 协议僵局 |
一、TCC核心思想:业务逻辑补偿
1. 三阶段操作解析
| Try | ||
| Confirm | ||
| Cancel |
举个栗子🌰:
正常流程:Try成功 → Confirm提交
异常流程:Try失败 → Cancel回滚
极端情况:Try成功但Confirm超时 → 定时任务重试
二、代码实战:手撕TCC三阶段
1. Try阶段:资源冻结
// 账户服务接口public interface AccountService {@Transactionalboolean 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通过业务层补偿,避免数据库长锁
资金操作必须保留中间状态(冻结金额),不允许直接扣减
五、面试暴击指南
致命问题:
“TCC的Cancel操作失败怎么办?”
答:“通过异步重试+告警人工介入,同时设计前期的资源预留足够覆盖逆操作”
高阶话术:
“我们在支付系统中采用TCC+本地消息表混合方案,Confirm阶段失败时,用消息队列保证最终提交”
MySQL要坐不住了!Vitess之父Sugu“投敌”Postgres造新数据库,这次真要掀翻桌子?
为什么DeepSeek火之后,人们想到的是大量裁员,而不是实行上三休四?
苹果“痛下杀手”弃Java,用自家语言Swift重写关键服务:内存减90%,性能增40%!
我的很多DBA朋友,都消失了...
3000亿芯片巨头海光信息拟合并900亿超算龙头中科曙光,国产算力产业的“王炸组合”
1000T的文件怎么能快速从南京传到北京?最佳方案你肯定想不到
麻了,领导让我写个脚本,把700T数据库全部迁移到别的机房,带宽不超过2Mbps
号外!《核心系统分布式数据库选型指南》电子书(附下载)正式上线
解锁数据架构现代化密码,《实时数仓选型指南》电子书(附下载)正式上线啦