大家好!最近我们深入分布式事务领域,从 CAP、BASE、2PC、3PC 等理论,终于来到实战环节。这篇笔记将带你用 Seata 的 TCC(Try-Confirm-Cancel)模式,搭建一个经典的跨服务银行转账 Demo,希望对你有所启发!😉
TCC 模式概述 🤔
- Try:检查业务可行性,预留资源(如冻结账户金额)。
- Confirm:所有服务的 Try 都成功后,确认之前预留的操作(如实际扣款)。
- Cancel:若任何一个 Try 失败,释放之前预留的资源(如解冻金额)。
优点:性能优于 XA,不依赖数据库强锁。
挑战:业务代码侵入性强,需要自行处理幂等、空回滚、资源悬挂等问题。
Seata 的优势 🌟
Seata 的 TCC 模式帮助我们简化:
- 事务协调:Seata Server (TC) 管理全局事务状态,自动驱动 Confirm/Cancel。
- 接口定义简化:通过
@LocalTCC
和@TwoPhaseBusinessAction
注解定义 TCC 接口。 - 自动空回滚:若 Try 未成功,框架自动跳过 Cancel 调用。
- 上下文传递:
BusinessActionContext
中携带 XID、分支 ID、业务参数,方便实现幂等。
注意:幂等与悬挂仍需开发者在 Confirm/Cancel 中实现额外逻辑。
实战 Demo:银行转账 TCC 版
技术栈
- Java 17
- Spring Boot 3.1.12
- Spring Data JPA (Hibernate)
- Seata 1.7.0
- H2 内存数据库
- Maven
环境准备
- 本地安装 Docker。
-
克隆项目:
git clone https://github.com/Wilsoncyf/seata-demo.git cd seata-demo
启动 Seata Server
docker run --name seata-server \-p 8091:8091 \-e SEATA_MODE=file \-d seataio/seata-server:1.7.0
关键代码解读
1. TCC 接口定义 (AccountTccAction.java
)
@LocalTCC
public interface AccountTccAction {@TwoPhaseBusinessAction(name = "AccountTccAction",commitMethod = "confirm",rollbackMethod = "cancel")boolean prepare(BusinessActionContext actionContext,@BusinessActionContextParameter(paramName = "userId") String userId,@BusinessActionContextParameter(paramName = "amount") BigDecimal amount);boolean confirm(BusinessActionContext actionContext);boolean cancel(BusinessActionContext actionContext);
}
@LocalTCC
:标记 TCC 接口。@TwoPhaseBusinessAction
:关联 Try、Confirm、Cancel 方法。@BusinessActionContextParameter
:指定跨阶段传递的参数。
2. TCC 接口实现 (AccountTccActionImpl.java
)
@Component("accountTccAction")
public class AccountTccActionImpl implements AccountTccAction {// 注入 Repository@Override@Transactionalpublic boolean prepare(BusinessActionContext ctx, String userId, BigDecimal amount) {// 冻结金额 (更新 frozenBalance)log.info("TCC Prepare - 冻结账户 {} 金额 {} 成功", userId, amount);return true;}@Override@Transactionalpublic boolean confirm(BusinessActionContext ctx) {String txId = ctx.getXid();String branchId = String.valueOf(ctx.getBranchId());// 幂等判断if (transactionLogRepository.existsByTxIdAndBranchId(txId, branchId)) return true;// 获取参数并扣款 + 记录日志transactionLogRepository.save(new TransactionLog(...));log.info("TCC Confirm - 确认账户 {} 扣款 {} 成功", userId, amount);return true;}@Override@Transactionalpublic boolean cancel(BusinessActionContext ctx) {String txId = ctx.getXid();String branchId = String.valueOf(ctx.getBranchId());// 幂等判断if (transactionLogRepository.existsByTxIdAndBranchId(txId, branchId)) return true;// 解冻逻辑 + 记录日志transactionLogRepository.save(new TransactionLog(...));log.info("TCC Cancel - 解冻账户 {} 金额 {} 成功", userId, amount);return true;}
}
- Try:冻结资产。
- Confirm:实际扣款 & 幂等日志。
- Cancel:释放冻结 & 幂等日志。
3. 事务发起方 (TransferService.java
)
@Service
public class TransferService {@GlobalTransactional(timeoutMills = 60000, name = "seata-tcc-transfer-demo")public void transfer(String fromUserId, String toUserId, BigDecimal amount) {log.info("Global Transaction Start...");boolean ok = accountTccAction.prepare(null, fromUserId, amount);if (!ok) throw new RuntimeException("冻结失败");log.info("TCC Try FROM 成功");// 可继续调用收款方 Try...log.info("Global Transaction End - All Try phases successful.");}
}
@GlobalTransactional
:开启全局事务。- 无需手动调用 confirm/cancel,Seata 自动处理。
测试结果
场景 | 日志流程 | 数据库状态 |
---|---|---|
成功转账 | Prepare → Confirm | 扣款 & 冻结一致 |
模拟异常回滚 | Prepare → Cancel | 解冻 & 回滚到初始状态 |
源码地址
🔗 GitHub - Wilsoncyf/seata-demo
更多运行、调试说明见仓库中的 README.md
。
总结与思考
- TCC vs XA/Saga/AT:根据一致性、性能、开发成本权衡选型。
- 幂等 & 悬挂:框架不完全覆盖,需在业务实现中处理。
- 扩展:可结合可靠消息、Saga 模式,满足不同场景。
欢迎在评论区交流问题与建议!💬