在分布式事务的治理实践中,将系统从 Seata 的 AT 模式迁移到 TCC 模式,绝非简单的配置切换。这背后,实际上是一次 事务设计哲学的根本性转变:
从 “框架自动补偿” 到 “业务显式补偿”
从 “全局锁保护一致性” 到 “资源预留实现隔离”
AT 模式的优势在于 低侵入性,开发便捷,但在高并发场景下,却容易遇到全局锁竞争与性能瓶颈的挑战。而 TCC 模式则以更高的业务复杂度为代价,换取了更强的性能与控制力。
本文将系统地梳理从 AT 模式迁移到 TCC 模式的核心认知、具体改造步骤、关键异常处理、性能优化策略以及生产环境的实践经验。
🧭 一、迁移前:理解 AT 与 TCC 的本质差异
在动手改造之前,我们必须清晰地认识到这两种模式在设计理念和实现机制上的核心差异。
| 维度 |
AT 模式 |
TCC 模式 |
| 侵入性 |
低(框架自动生成反向 SQL) |
高(需手动实现 Try-Confirm-Cancel 三阶段) |
| 资源锁 |
持有全局锁直到整个分布式事务结束 |
业务层面通过资源预留实现隔离 |
| 一致性 |
强一致性(依赖底层数据库的本地事务) |
最终一致性 |
| 支持资源 |
主要支持关系型数据库(如 MySQL) |
任意资源(数据库、Redis、消息队列、NoSQL 等) |
| 典型问题 |
锁冲突、事务回滚时长(RT)抖动 |
业务逻辑复杂度高,设计难度大 |
| 适用场景 |
并发冲突较低的业务 |
高并发、高性能要求、跨多种资源的事务场景 |
🚦 二、什么时候必须考虑迁移到 TCC?
如果你的系统出现以下信号,那么迁移到 TCC 模式就应该被提上日程了:
✔ 高并发下,全局锁等待严重,成为性能瓶颈。
✔ undo_log 表数据膨胀,清理压力大。
✔ 存在热点资源冲突(例如库存扣减、账户余额、配额控制)。
✔ 事务操作需要涉及 Redis、消息队列或第三方系统等非数据库资源。
如果符合上述情况,TCC 将是更合适的解决方案,它能帮助你突破 AT 模式在性能和资源类型支持上的限制。
🛠 三、迁移实战:四步改造流程
✅ 第一步:数据表结构改造(引入资源预留)
AT 模式依赖 undo_log 表记录数据快照以实现回滚;TCC 模式则依赖 业务层面的资源冻结 来保证最终一致性。
示例:账户表改造
AT 模式下的表结构
account(id, user_id, balance)
TCC 模式下的表结构
CREATE TABLE account (
id BIGINT PRIMARY KEY,
user_id VARCHAR(32),
balance DECIMAL(10,2), -- 可用余额
frozen_amount DECIMAL(10,2) -- 冻结金额
);
关键变化是增加了 frozen_amount 字段,用于在 Try 阶段预留资源,而不直接扣减 balance。
✅ 引入 Fence 表(防悬挂、幂等、空回滚)
强烈建议创建一张防悬挂日志表(Fence Log),它是处理 TCC 三大核心异常(幂等、空回滚、悬挂)的基础设施。
CREATE TABLE tcc_fence_log (
xid VARCHAR(128),
branch_id BIGINT,
action_name VARCHAR(64),
status TINYINT,
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (xid, branch_id)
);
✅ 第二步:定义 TCC 接口(三阶段方法)
在 Java 服务中,我们需要定义一个接口,明确声明 Try、Confirm、Cancel 三个方法。
@LocalTCC
public interface AccountTccService {
@TwoPhaseBusinessAction(
name = "accountTry",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
void tryDebit(
@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
✅ 第三步:实现 Try / Confirm / Cancel 逻辑
🥇 Try 阶段:仅进行资源预留
Try 阶段的核心是“预留”资源,而不是“提交”业务。它必须是一个本地事务。
@Transactional
public void tryDebit(String userId, BigDecimal amount) {
String xid = RootContext.getXID();
Long branchId = RootContext.getBranchId();
// 防悬挂检查:如果已存在悬挂标记,则拒绝执行
if (fenceLogDAO.isSuspended(xid, branchId)) {
return;
}
Account account = accountDAO.findByUserId(userId);
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 核心操作:冻结资源(balance - amount, frozen_amount + amount)
accountDAO.freezeBalance(userId, amount);
// 记录 Try 阶段已执行
fenceLogDAO.insertTry(xid, branchId);
}
🥈 Confirm 阶段:真正提交,必须幂等且不失败
Confirm 阶段执行真正的业务提交。设计上必须保证其幂等性和最终成功(业务上不允许失败)。
@Transactional
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
Long branchId = context.getBranchId();
// 幂等检查:如果已提交,直接返回成功
if (fenceLogDAO.isCommitted(xid, branchId)) {
return true;
}
String userId = (String) context.getActionContext("userId");
BigDecimal amount = (BigDecimal) context.getActionContext("amount");
// 核心操作:确认冻结,将 frozen_amount 清零
accountDAO.confirmFreeze(userId, amount);
fenceLogDAO.markCommitted(xid, branchId);
return true;
}
🥉 Cancel 阶段:资源释放,最终安全网
Cancel 阶段是资源释放和回滚的保障,必须处理“空回滚”场景,并保证幂等。
@Transactional
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
Long branchId = context.getBranchId();
// 空回滚处理:如果 Try 未执行,则插入悬挂标记并返回
if (!fenceLogDAO.isTryExecuted(xid, branchId)) {
fenceLogDAO.insertSuspended(xid, branchId);
return true;
}
// 幂等检查:如果已回滚,直接返回成功
if (fenceLogDAO.isRollbacked(xid, branchId)) {
return true;
}
String userId = (String) context.getActionContext("userId");
BigDecimal amount = (BigDecimal) context.getActionContext("amount");
// 核心操作:释放冻结的资源(frozen_amount - amount, balance + amount)
accountDAO.unfreezeBalance(userId, amount);
fenceLogDAO.markRollbacked(xid, branchId);
return true;
}
✅ 第四步:事务发起方调整
在全局事务的入口处,我们不再直接调用普通的业务方法,而是调用 TCC 服务的 Try 方法。
@GlobalTransactional
public void placeOrder(...) {
accountTccService.tryDebit(userId, amount);
orderTccService.tryCreate(orderId);
// ... 其他参与者的 Try 操作
}
⚠️ 四、TCC 三大异常必须处理
这是 TCC 模式稳定性的基石,上文代码中已通过 Fence 表体现。
- 空回滚:Cancel 请求在 Try 请求之前到达。解决方案:Cancel 时检查 FenceLog,若 Try 未执行,则记录悬挂标记后直接返回。
- 幂等:Confirm 或 Cancel 因网络等原因被重复调用。解决方案:通过 FenceLog 的状态判断,若已执行过,则直接返回成功。
- 悬挂:Cancel 执行了空回滚并记录了悬挂标记后,Try 请求才到达。解决方案:Try 执行前检查是否存在悬挂标记,若存在则拒绝执行。
🚀 五、性能优化关键点
- ✅ Try 阶段必须极简:通常应只是一条 UPDATE 语句,避免复杂查询、计算和远程调用,以缩短全局锁持有时间。
- ✅ 冻结字段设计要简单:推荐使用
balance 和 frozen_amount 这种清晰的数值字段。避免使用复杂的多状态机或 JSON 结构字段,它们会显著增加业务逻辑的复杂度。
- ✅ Confirm 阶段不依赖瞬时资源:Confirm 的逻辑设计不应依赖于可能变化的临时状态(如缓存)或易失效的 Token,确保其在任何时候都能执行成功。
🔍 六、迁移策略(生产经验)
- ✅ 优先迁移热点资源服务:如库存、账户、限流配额等最易产生并发冲突的服务。
- ✅ AT 与 TCC 混用,渐进式改造:这是一个稳妥的策略。核心、高频链路采用 TCC 以提升性能,边缘、低频链路暂时保留 AT 以控制改造范围。
- ✅ 避免嵌套事务陷阱:在 TCC 服务的 Try、Confirm、Cancel 方法内部,应保持清晰的本地事务边界,不要再嵌套使用
@GlobalTransactional 注解。
📊 七、监控与运维(90% 团队忽略)
必须监控以下指标:
✔ Try / Confirm / Cancel 各个阶段的 QPS 和耗时。
✔ 二阶段(Confirm/Cancel)的重试次数和成功率。
✔ FenceLog 表的记录增长率和数据清理情况。
✔ 业务表中冻结资源(如 frozen_amount)的总量及变化趋势。
必须设置以下告警:
🚨 Confirm 阶段重试次数突然暴涨(可能预示业务逻辑设计问题)。
🚨 Cancel 阶段执行比例异常升高(可能预示 Try 阶段失败率增加)。
🚨 冻结资源长时间未释放(可能导致业务资损或可用性风险)。
🧪 八、测试策略(TCC 成败关键)
| ✅ 专项异常测试:必须模拟并验证三大异常场景的处理是否正确。 |
场景 |
验证点 |
| 空回滚 |
不执行 Try,直接调用 Cancel,检查资源是否被误扣。 |
| 幂等 |
对同一事务分支,重复调用 Confirm 或 Cancel,检查资源状态是否被多次修改。 |
| 悬挂 |
先执行 Cancel(触发空回滚),再延迟执行 Try,检查 Try 是否被正确拒绝。 |
✅ Chaos 工程测试:在生产准环境或压测环境中,模拟网络延迟、TC(事务协调器)宕机、Confirm 请求丢包等故障,验证整个 TCC 事务链路的最终一致性和自愈能力。
🔧 九、配置变更注意事项
- ✅ 移除对
undo_log 的依赖:TCC 模式不再需要 Seata 自动生成反向 SQL,相关表的自动创建和清理配置可以移除。
- ✅ 数据源代理(DataSourceProxy):在纯 TCC 模式下,业务补偿不依赖框架对 SQL 的拦截和代理,因此可以不再使用
DataSourceProxy 包装数据源(除非混合使用 AT 模式)。这能减少一点性能开销。
- ✅ 确保本地事务生效:务必检查并保证 Try、Confirm、Cancel 方法上的
@Transactional 注解能够正确开启和提交本地数据库事务。
🧠 十、核心认知升级
从 AT 迁移到 TCC,本质上是从 “相信框架帮你处理回滚” 转变为 “业务自己设计兜底方案”。
TCC 模式的精髓在于:
用更高的设计复杂度,换取极致的性能和一致性控制力。
它迫使开发者深入思考业务的最终状态和补偿路径,这是一种设计能力的锻炼,也是构建高可靠分布式系统的关键一步。在这个过程中,参考 云栈社区 上其他关于 分布式系统 架构的讨论,可能会带来新的启发。
✅ 十一、经验总结
✔ Try 只预留,不做最终业务提交。
✔ Confirm 必须设计成幂等且业务上永不失败。
✔ Cancel 必须是永远安全可执行的最后保障。
✔ FenceLog 必上,它是处理异常的生命线。
✔ 幂等性是生命线,贯穿 Confirm 和 Cancel 阶段。
✔ 采用渐进式改造,优先迁移热点资源服务。
✔ 混合模式(AT+TCC)是平滑迁移的推荐路径。
✔ 上线前务必经过充分的灰度发布、压力测试和混沌测试。
希望这份从 Seata AT 模式迁移到 TCC 模式的实战指南,能帮助你在应对高并发挑战、优化系统性能的道路上,做出更合适的技术决策。任何架构的演进都需要权衡,理解其背后的设计思想,才能运用得当。