TCC事务深入解析和应用
彻底搞懂 TCC:微服务分布式事务的“特种部队”
在分布式事务的江湖中,如果说 Seata 的 AT 模式是“自动步枪”(简单好用但有性能损耗),那么 TCC 模式就是“狙击枪”(操作复杂但精准高效)。
很多开发者听说过 TCC,但往往止步于“Try-Confirm-Cancel”的概念。今天,我们将深入底层,结合 Spring Cloud 和 Seata,手把手教你如何落地一个生产级的 TCC 方案,并解决最让人头疼的“悬挂”和“空回滚”问题。
为什么我们需要 TCC?
在微服务架构中,AT 模式(基于数据库本地锁)虽然开发简单,但在高并发、热点账户(如秒杀扣库存)场景下,全局锁会导致性能瓶颈。
TCC(Try-Confirm-Cancel)应运而生。它不依赖数据库的全局锁,而是将锁的粒度交给业务代码控制。
- 性能更高:不长时间占用数据库连接。
- 控制更细:你可以自定义锁的粒度(例如:冻结余额字段,而不是锁住整行记录)。
TCC 的“三板斧”原理
TCC 将一个业务操作拆分为三个阶段:
- Try (资源预留):
- 做业务检查(一致性)。
- 预留业务资源(准隔离性)。
- 例子:把 100 元从“余额”移动到“冻结金额”。
- Confirm (业务确认):
- 不做任何检查,利用 Try 阶段预留的资源执行真正业务。
- 要求幂等。
- 例子:把“冻结金额”扣除。
- Cancel (业务取消):
- 释放 Try 阶段预留的资源。
- 要求幂等。
- 例子:把“冻结金额”退回到“余额”。
实战:基于 Seata 的 TCC 模式实现
假设我们有一个库存服务,需要实现 TCC 模式。
1. 接口定义 (@TwoPhaseBusinessAction)
在 TCC 模式下,我们需要在接口上使用 Seata 的注解来定义这三个方法。
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface InventoryTccService {
/**
* Try 阶段:预留库存
* @param productId 商品ID
* @param count 扣减数量
* @return 成功与否
*/
@TwoPhaseBusinessAction(name = "prepareInventory", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "count") int count);
/**
* Confirm 阶段:提交
*/
boolean commit(BusinessActionContext actionContext);
/**
* Cancel 阶段:回滚
*/
boolean rollback(BusinessActionContext actionContext);
}
2. 业务实现(关键逻辑)
这里是 TCC 的精髓所在:资源隔离。我们需要在数据库表中增加一个 frozen_stock(冻结库存)字段。
@Service
@Slf4j
public class InventoryTccServiceImpl implements InventoryTccService {
@Autowired
private InventoryMapper inventoryMapper;
// --- Try 阶段 ---
@Override
@Transactional(rollbackFor = Exception.class)
public boolean prepare(BusinessActionContext actionContext, String productId, int count) {
// 1. 检查库存是否充足
Inventory inventory = inventoryMapper.selectById(productId);
if (inventory.getStock() < count) {
throw new RuntimeException("库存不足");
}
// 2. 预留资源(stock - count, frozen + count)
// 关键:必须防止“悬挂”(后面会讲)
inventoryMapper.freezeStock(productId, count);
log.info("Try阶段:冻结库存成功,xid: {}", actionContext.getXid());
return true;
}
// --- Confirm 阶段 ---
@Override
@Transactional(rollbackFor = Exception.class)
public boolean commit(BusinessActionContext actionContext) {
// 获取 Try 阶段传入的参数
String productId = (String) actionContext.getActionContext("productId");
int count = (int) actionContext.getActionContext("count");
// 3. 正式扣减(frozen - count)
// 关键:幂等性控制
int updateCount = inventoryMapper.commitStock(productId, count);
log.info("Confirm阶段:扣减库存完成");
return true;
}
// --- Cancel 阶段 ---
@Override
@Transactional(rollbackFor = Exception.class)
public boolean rollback(BusinessActionContext actionContext) {
String productId = (String) actionContext.getActionContext("productId");
int count = (int) actionContext.getActionContext("count");
// 4. 解冻资源(stock + count, frozen - count)
// 关键:防空回滚、幂等性
inventoryMapper.unfreezeStock(productId, count);
log.info("Cancel阶段:解冻库存完成");
return true;
}
}
3. 发起全局事务
在订单服务(调用方)中,依然使用 @GlobalTransactional,这与 AT 模式一致,Seata 会自动感知下游是 TCC 资源。
@GlobalTransactional
public void createOrder(Order order) {
// 1. 调用库存 TCC 的 Try 方法
inventoryTccService.prepare(null, order.getProductId(), order.getCount());
// 2. 创建订单
orderRepository.save(order);
// 如果这里报错,Seata 会自动调用 inventoryTccService.rollback()
}
进阶:TCC 的“三大坑”与解决方案
这部分是你在生产环境必须解决的问题,否则数据必乱。
1. 幂等性(Idempotency)
- 问题:网络抖动导致 Seata 重复调用 Confirm 或 Cancel。
- 解决:在 Confirm/Cancel 阶段,通过数据库唯一索引(如
tcc_log表,以xid + branchId为主键)记录执行状态。执行前先查询,如果已执行则直接返回成功。
2. 空回滚(Empty Rollback)
- 问题:Try 阶段因为网络拥堵没请求到 TCC 服务,或者 Try 阶段直接抛异常了(还没落地数据)。此时 Seata 全局事务回滚,触发 Cancel。如果 Cancel 直接执行“解冻库存”,因为 Try 没执行,库存没冻结,导致库存凭空增加。
- 解决:Cancel 方法执行时,必须识别出 Try 是否执行过。如果 Try 没执行,Cancel 直接返回成功(不做操作)。
3. 悬挂(Hanging)
- 问题:网络拥堵,Try 请求超时。Seata 判定失败,触发 Cancel。Cancel 先执行完了。结果,那条迟到的 Try 请求到了!此时 Try 执行冻结库存,但因为全局事务已经结束,这笔冻结的库存永远不会被 Confirm 或 Cancel,被称为“悬挂”。
- 解决:Try 方法执行时,需要检查当前事务 ID 是否已经执行过 Cancel。如果已执行过 Cancel,Try 必须拒绝执行。
终极方案:事务控制表
为了解决上述三个问题,通常需要一张辅助表 local_tcc_log:
- Try 逻辑:插入一条记录(状态:TRYING)。如果插入失败(因为 Cancel 已经插入了记录),则抛异常(防悬挂)。
- Confirm 逻辑:更新记录状态为 CONFIRMED。如果记录不存在,说明 Try 没成功(理论上不可能进入 Confirm)。
- Cancel 逻辑:插入或更新记录(状态:CANCELLED)。如果记录不存在,说明 Try 没到,插入一条空回滚记录(防空回滚)。
总结:何时选择 TCC?
TCC 不是银弹,它的开发成本比 AT 模式高出 3 倍以上(每个接口写三个方法,还要处理异常)。
选用建议:
- 首选 AT 模式:绝大多数中后台业务,并发不超过 1000 QPS,优先用 Seata AT,开发效率第一。
- 必选 TCC 的场景:
- 核心资金流:涉及金钱往来,必须精准控制状态,不希望依赖数据库全局锁。
- 高性能并发:如秒杀库存扣减,需要极致的数据库性能。
- 无数据库事务:被调用的服务(如 Redis、第三方 API)不支持 JDBC 事务,只能通过代码逻辑回滚。
TCC 模式通过业务层面的精细化控制,实现了分布式环境下的数据一致性。虽然实现复杂,但它是构建高可靠、高性能微服务系统的必备技能。
浙公网安备 33010602011771号