TCC 事务原理

某电商平台的双十一订单系统,需要保证下单、扣库存、扣余额的原子性。

团队最开始使用了 Seata 的 AT 模式(自动补偿型分布式事务),但在压测中发现:由于锁住了太多行,系统的并发能力大幅下降。

团队改用 TCC 模式(手动补偿型分布式事务)。通过业务层面的 Try(预留资源)、Confirm(确认使用)、Cancel(释放资源),将锁的粒度从"行锁"降到了"资源预留"级别。

结果:并发能力恢复到单机水平,TPS 从 500 提升到 5000。

【架构权衡】 TCC 是"业务补偿型"分布式事务,与 2PC 的"数据库锁定型"完全不同。TCC 通过业务层面的 Try-Confirm-Cancel,将分布式事务的锁从数据库层面提升到了业务层面,大大提高了并发能力。但 TCC 对业务代码有侵入性,需要每个业务操作都实现 Try-Confirm-Cancel 三个方法。


一、核心问题 🔴

1.1 TCC vs 2PC

2PC(数据库层面):
├─ 通过数据库锁保证原子性
├─ 锁粒度:行锁/表锁
├─ 对业务代码无侵入
├─ 缺点:锁时间长,并发受限
└─ 代表:Seata AT 模式

TCC(业务层面):
├─ 通过业务补偿保证原子性
├─ 锁粒度:业务预留资源
├─ 对业务代码有侵入
├─ 缺点:需要实现 Try-Confirm-Cancel
└─ 优点:锁时间短,并发能力高

1.2 TCC 的三个阶段

┌─────────────────────────────────────────────────────────────────┐
│                      阶段一:Try(预留资源)                        │
│                                                                  │
│  目的:检查业务可行性,预留业务资源                              │
│                                                                  │
│  示例:                                                          │
│  ├─ 订单服务:创建"待确认"订单                                  │
│  ├─ 库存服务:冻结库存(但不真正扣减)                          │
│  └─ 余额服务:冻结余额(但不真正扣减)                          │
│                                                                  │
│  特点:                                                          │
│  ├─ 所有参与者都执行 Try                                        │
│  ├─ 如果任何一个 Try 失败,发送 Cancel 给所有参与者            │
│  └─ 锁粒度:冻结资源(不影响其他业务)                          │
└─────────────────────────────────────────────────────────────────┘

                                  ▼ 全部 Try 成功
┌─────────────────────────────────────────────────────────────────┐
│                    阶段二:Confirm(确认使用)                    │
│                                                                  │
│  目的:真正执行业务操作                                         │
│                                                                  │
│  示例:                                                          │
│  ├─ 订单服务:确认订单,状态改为"已创建"                         │
│  ├─ 库存服务:真正扣减被冻结的库存                               │
│  └─ 余额服务:真正扣减被冻结的余额                              │
│                                                                  │
│  特点:                                                          │
│  ├─ Confirm 操作必须幂等                                        │
│  ├─ 如果 Confirm 失败,不断重试                                  │
│  └─ 如果长时间无法 Confirm,进入人工处理                        │
└─────────────────────────────────────────────────────────────────┘

                                  ▼ 任意 Confirm 失败
┌─────────────────────────────────────────────────────────────────┐
│                    阶段三:Cancel(释放资源)                     │
│                                                                  │
│  目的:回滚业务操作,释放预留资源                               │
│                                                                  │
│  示例:                                                          │
│  ├─ 订单服务:取消"待确认"订单                                  │
│  ├─ 库存服务:解冻被冻结的库存                                  │
│  └─ 余额服务:解冻被冻结的余额                                  │
│                                                                  │
│  特点:                                                          │
│  ├─ Cancel 操作必须幂等                                          │
│  ├─ 如果 Confirm 已成功,不执行 Cancel                          │
│  └─ 如果 Cancel 失败,不断重试                                    │
└─────────────────────────────────────────────────────────────────┘

二、方案对比

2.1 TCC 的代码实现

// TCC 接口定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TccTransaction {
}

public interface TccAction {
    // Try:预留资源
    boolean tryAction(Context context);

    // Confirm:确认使用
    boolean confirmAction(Context context);

    // Cancel:释放资源
    boolean cancelAction(Context context);
}

// 订单服务的 TCC 实现
@Service
public class OrderTccAction implements TccAction {

    @Autowired
    private OrderRepository orderRepository;

    @Override
    public boolean tryAction(Context context) {
        String xid = context.getXid();
        String orderId = context.get("orderId");
        BigDecimal amount = context.get("amount");

        // 1. 创建"待确认"订单
        Order order = new Order();
        order.setId(orderId);
        order.setAmount(amount);
        order.setStatus(OrderStatus.PENDING);
        order.setXid(xid);
        orderRepository.save(order);

        // 2. 记录 TCC 分支事务
        tccLogService.log(xid, "OrderTccAction", "TRY");

        return true;
    }

    @Override
    public boolean confirmAction(Context context) {
        String orderId = context.get("orderId");

        // 确认订单:状态改为"已创建"
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CREATED);
        orderRepository.update(order);

        tccLogService.log(context.getXid(), "OrderTccAction", "CONFIRM");
        return true;
    }

    @Override
    public boolean cancelAction(Context context) {
        String orderId = context.get("orderId");

        // 取消订单:状态改为"已取消"
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.update(order);

        tccLogService.log(context.getXid(), "OrderTccAction", "CANCEL");
        return true;
    }
}

// 库存服务的 TCC 实现
@Service
public class InventoryTccAction implements TccAction {

    @Autowired
    private InventoryRepository inventoryRepository;

    @Override
    public boolean tryAction(Context context) {
        String itemId = context.get("itemId");
        int count = context.get("count");

        // 冻结库存(不真正扣减)
        // UPDATE inventory SET frozen = frozen + count WHERE id = itemId
        int affected = inventoryRepository.freezeStock(itemId, count);
        if (affected == 0) {
            return false; // 库存不足,Try 失败
        }

        return true;
    }

    @Override
    public boolean confirmAction(Context context) {
        String itemId = context.get("itemId");
        int count = context.get("count");

        // 真正扣减冻结的库存
        // UPDATE inventory SET frozen = frozen - count, stock = stock - count WHERE id = itemId
        inventoryRepository.deductFrozenStock(itemId, count);

        return true;
    }

    @Override
    public boolean cancelAction(Context context) {
        String itemId = context.get("itemId");
        int count = context.get("count");

        // 解冻库存(回滚 Try)
        // UPDATE inventory SET frozen = frozen - count WHERE id = itemId
        inventoryRepository.unfreezeStock(itemId, count);

        return true;
    }
}

2.2 TCC 协调者实现

// TCC 协调者(简化版)
@Service
public class TccCoordinator {

    @Autowired
    private List<TccAction> actions;

    public boolean executeTccTransaction(String xid, Map<String, Object> params) {
        // 阶段一:执行 Try
        Map<String, Boolean> tryResults = new HashMap<>();
        for (TccAction action : actions) {
            Context context = new Context(xid, params);
            try {
                boolean success = action.tryAction(context);
                tryResults.put(action.getClass().getName(), success);
            } catch (Exception e) {
                tryResults.put(action.getClass().getName(), false);
            }
        }

        // 如果所有 Try 都成功,执行 Confirm
        if (tryResults.values().stream().allMatch(r -> r)) {
            confirmAll(xid);
            return true;
        } else {
            // 任意一个 Try 失败,执行 Cancel
            cancelAll(xid);
            return false;
        }
    }

    private void confirmAll(String xid) {
        for (TccAction action : actions) {
            Context context = loadContext(xid);
            try {
                action.confirmAction(context);
            } catch (Exception e) {
                // Confirm 失败,不断重试
                retryLater(action, "CONFIRM", xid);
            }
        }
    }

    private void cancelAll(String xid) {
        for (TccAction action : actions) {
            Context context = loadContext(xid);
            try {
                action.cancelAction(context);
            } catch (Exception e) {
                // Cancel 失败,不断重试
                retryLater(action, "CANCEL", xid);
            }
        }
    }
}

【架构权衡】 TCC 的核心挑战是"幂等性"和"空回滚":

  • 幂等性:Try、Confirm、Cancel 都可能被调用多次,必须保证幂等
  • 空回滚:Try 还没执行,Cancel 就被调用了(网络分区导致),需要处理

三、TCC 的关键技术问题

3.1 幂等性保证

// 每个 TCC 操作都必须幂等
@Service
public class IdempotentInventoryTccAction implements TccAction {

    @Autowired
    private TccLogRepository tccLogRepository;

    @Override
    public boolean confirmAction(Context context) {
        String xid = context.getXid();
        String actionName = "InventoryTccAction";

        // 检查是否已经 Confirm 过(幂等性保证)
        if (tccLogRepository.exists(xid, actionName, "CONFIRM")) {
            return true; // 幂等:已经 Confirm 过,直接返回成功
        }

        // 执行真正的 Confirm
        String itemId = context.get("itemId");
        int count = context.get("count");
        inventoryRepository.deductFrozenStock(itemId, count);

        // 记录 Confirm 日志
        tccLogRepository.save(xid, actionName, "CONFIRM");

        return true;
    }
}

3.2 空回滚处理

// 空回滚:Try 还没执行,Cancel 就被调用了
@Override
public boolean cancelAction(Context context) {
    String xid = context.getXid();
    String actionName = "InventoryTccAction";

    // 检查 Try 是否执行过
    if (!tccLogRepository.exists(xid, actionName, "TRY")) {
        // Try 未执行,这是空回滚
        // 记录日志,但不做任何操作
        tccLogRepository.save(xid, actionName, "CANCEL_EMPTY");
        return true;
    }

    // Try 执行过,执行真正的 Cancel
    String itemId = context.get("itemId");
    int count = context.get("count");
    inventoryRepository.unfreezeStock(itemId, count);

    tccLogRepository.save(xid, actionName, "CANCEL");
    return true;
}

四、工程代价评估

维度评估
实现复杂度高(需要实现 Try-Confirm-Cancel)
业务侵入性高(每个业务操作都要实现 TCC 接口)
锁粒度细(业务层面,可控)
并发能力高(无数据库锁)
一致性最终一致
补偿可靠性高(Try-Confirm-Cancel 三阶段)
适用场景跨服务、跨数据库的分布式事务

五、落地 Checklist

  • 业务改造:识别需要分布式事务的业务场景
  • TCC 实现:为每个参与者实现 Try-Confirm-Cancel
  • 幂等设计:每个 TCC 操作都要幂等
  • 空回滚处理:处理 Try 未执行但 Cancel 被调用的情况
  • 重试机制:Confirm/Cancel 失败后的重试策略
  • 监控告警:监控超时未 Confirm/Cancel 的事务
  • 人工补偿:长时间未完成的事务的人工处理流程