Java中的TCC模式:Try/Confirm/Cancel三个阶段的业务逻辑实现与状态管理
各位朋友,大家好!今天我们来深入探讨一种分布式事务解决方案——TCC (Try-Confirm-Cancel) 模式。在微服务架构日益普及的今天,跨多个服务的事务一致性是一个核心挑战。TCC 模式提供了一种相对灵活且可控的方式来解决这个问题,它通过将业务逻辑拆分为三个阶段,允许我们在服务层面进行补偿,从而达到最终一致性。
1. TCC模式概述
TCC 是一种补偿型事务,其核心思想是将一个完整的业务操作分解为以下三个阶段:
- 
Try 阶段: 尝试执行业务,完成所有业务检查(一致性),预留所需的业务资源(准隔离性)。Try 阶段如果成功,则表示资源预留成功,为后续的 Confirm 阶段做准备。如果 Try 阶段失败,则不需要执行 Cancel 阶段,直接回滚即可。
 - 
Confirm 阶段: 确认执行业务,在 Try 阶段预留的资源基础上,真正执行业务操作。Confirm 阶段执行成功,则事务成功完成。Confirm 阶段通常被认为是成功的概率非常高,因此 Confirm 阶段一般不做任何业务检查,直接执行。如果 Confirm 阶段失败,需要引入重试机制。
 - 
Cancel 阶段: 取消执行业务,释放 Try 阶段预留的业务资源。Cancel 阶段是在 Try 阶段执行成功,但后续业务失败的情况下执行的,用于回滚 Try 阶段的操作。Cancel 阶段需要保证幂等性,防止重复执行导致数据不一致。
 
2. TCC与ACID事务的对比
| 特性 | ACID 事务 | TCC 事务 | 
|---|---|---|
| 一致性 | 强一致性,保证事务的原子性 | 最终一致性,通过补偿机制保证数据最终一致 | 
| 隔离性 | 数据库的隔离级别(读已提交、可重复读等) | 应用层面的隔离,通过业务逻辑实现隔离 | 
| 持久性 | 事务提交后数据永久保存 | 依赖于各阶段的持久化,需保证各阶段的可靠性 | 
| 原子性 | 事务要么全部成功,要么全部失败 | 通过 Try/Confirm/Cancel 三个阶段实现原子性 | 
| 适用场景 | 单个数据库的事务,对一致性要求极高的场景 | 分布式事务,允许最终一致性,对性能有要求的场景 | 
3. TCC模式的实现要点
- 
幂等性控制: Confirm 和 Cancel 阶段必须保证幂等性。这意味着无论执行多少次,其结果都应该相同。通常可以通过在 TCC 执行记录中添加唯一事务ID,并在执行操作前检查该事务ID是否已经执行过来实现。
 - 
空回滚处理: 在某些情况下,Try 阶段可能没有执行,但 Cancel 阶段被触发。这是因为在调用 Try 阶段之前,分布式事务协调器可能已经判定事务失败并开始执行回滚。此时,Cancel 阶段需要能够处理这种情况,保证不抛出异常。
 - 
悬挂处理: 在某些极端情况下,Confirm 阶段在 Cancel 阶段之后执行。这意味着 Try 阶段预留的资源已经被释放,但 Confirm 阶段仍然尝试使用这些资源。为了避免这种情况,需要在 Confirm 阶段检查资源是否仍然可用,如果不可用,则直接忽略 Confirm 阶段的执行。
 - 
事务日志: 需要记录 TCC 事务的执行状态,以便在出现异常时进行恢复。事务日志可以存储在数据库中,也可以存储在专门的事务日志服务中。
 
4. 代码示例:订单服务与库存服务的TCC
假设我们有一个订单服务和一个库存服务,用户下单的流程涉及到这两个服务。我们使用 TCC 模式来保证订单创建和库存扣减的事务一致性。
4.1 订单服务
// 订单服务接口
public interface OrderService {
    boolean tryCreateOrder(String orderId, String productId, int quantity);
    boolean confirmCreateOrder(String orderId);
    boolean cancelCreateOrder(String orderId);
}
// 订单服务实现类
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private StockService stockService;
    @Autowired
    private OrderRepository orderRepository;
    @Override
    @Transactional // 保证本地订单操作的原子性
    public boolean tryCreateOrder(String orderId, String productId, int quantity) {
        // 1. 检查订单是否存在
        if (orderRepository.existsById(orderId)) {
            return false; // 订单已存在
        }
        // 2. 创建预订单,状态为 "TRY"
        Order order = new Order();
        order.setOrderId(orderId);
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setStatus("TRY");
        orderRepository.save(order);
        // 3. 尝试扣减库存
        boolean stockTryResult = stockService.tryDeductStock(productId, quantity, orderId); // 传入 orderId 作为事务ID
        if (!stockTryResult) {
            // 库存预扣减失败,需要回滚本地订单
            throw new RuntimeException("库存预扣减失败"); // 抛出异常,触发本地事务回滚
        }
        return true; // 订单预创建成功,库存预扣减成功
    }
    @Override
    @Transactional
    public boolean confirmCreateOrder(String orderId) {
        // 1. 检查订单是否存在,并且状态为 "TRY"
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null || !"TRY".equals(order.getStatus())) {
            return false; // 订单不存在或者状态不正确
        }
        // 2. 更新订单状态为 "CONFIRM"
        order.setStatus("CONFIRM");
        orderRepository.save(order);
        // 3. 确认扣减库存 (异步执行,降低confirm阶段的耗时)
        new Thread(() -> {
            boolean stockConfirmResult = stockService.confirmDeductStock(order.getProductId(), order.getQuantity(), orderId);
            if (!stockConfirmResult) {
                //TODO:  需要记录失败日志,并进行重试或人工干预。这里暂时省略重试逻辑
                System.err.println("库存确认扣减失败,订单ID: " + orderId);
            }
        }).start();
        return true; // 订单确认成功
    }
    @Override
    @Transactional
    public boolean cancelCreateOrder(String orderId) {
        // 1. 检查订单是否存在
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null) {
            return true; // 订单不存在,无需取消 (空回滚)
        }
        // 2. 释放预留的库存
        boolean stockCancelResult = stockService.cancelDeductStock(order.getProductId(), order.getQuantity(), orderId);
        if (!stockCancelResult) {
             //TODO:  需要记录失败日志,并进行重试或人工干预。这里暂时省略重试逻辑
            System.err.println("库存取消扣减失败,订单ID: " + orderId);
        }
        // 3. 删除订单记录
        orderRepository.deleteById(orderId);
        return true; // 订单取消成功
    }
}
// 订单实体类
@Entity
@Table(name = "orders")
@Data
public class Order {
    @Id
    private String orderId;
    private String productId;
    private int quantity;
    private String status; // TRY, CONFIRM, CANCEL
}
// 订单Repository
public interface OrderRepository extends JpaRepository<Order, String> {
}
4.2 库存服务
// 库存服务接口
public interface StockService {
    boolean tryDeductStock(String productId, int quantity, String transactionId);
    boolean confirmDeductStock(String productId, int quantity, String transactionId);
    boolean cancelDeductStock(String productId, int quantity, String transactionId);
}
// 库存服务实现类
@Service
public class StockServiceImpl implements StockService {
    @Autowired
    private StockRepository stockRepository;
    @Autowired
    private StockDeductLogRepository stockDeductLogRepository;
    @Override
    @Transactional
    public boolean tryDeductStock(String productId, int quantity, String transactionId) {
        // 1. 检查库存是否足够
        Stock stock = stockRepository.findById(productId).orElse(null);
        if (stock == null || stock.getStock() < quantity) {
            return false; // 库存不足
        }
        // 2. 预扣减库存 (实际不扣减,只是冻结)
        stock.setFrozenStock(stock.getFrozenStock() + quantity);
        stockRepository.save(stock);
        // 3. 记录预扣减日志
        StockDeductLog stockDeductLog = new StockDeductLog();
        stockDeductLog.setTransactionId(transactionId);
        stockDeductLog.setProductId(productId);
        stockDeductLog.setQuantity(quantity);
        stockDeductLog.setStatus("TRY");
        stockDeductLogRepository.save(stockDeductLog);
        return true; // 库存预扣减成功
    }
    @Override
    @Transactional
    public boolean confirmDeductStock(String productId, int quantity, String transactionId) {
        // 1. 检查预扣减日志是否存在,且状态为 TRY
        StockDeductLog stockDeductLog = stockDeductLogRepository.findByTransactionId(transactionId).orElse(null);
        if (stockDeductLog == null || !"TRY".equals(stockDeductLog.getStatus())) {
            return false; // 预扣减日志不存在或者状态不正确 (幂等性)  也可能是悬挂,需要处理
        }
        // 2. 实际扣减库存
        Stock stock = stockRepository.findById(productId).orElse(null);
        if (stock == null || stock.getFrozenStock() < quantity) {
            return false; // 库存不存在或者冻结库存不足,可能是cancel已经执行了 (悬挂) 需要处理
        }
        stock.setStock(stock.getStock() - quantity);
        stock.setFrozenStock(stock.getFrozenStock() - quantity);
        stockRepository.save(stock);
        // 3. 更新预扣减日志状态为 CONFIRM
        stockDeductLog.setStatus("CONFIRM");
        stockDeductLogRepository.save(stockDeductLog);
        return true; // 库存确认扣减成功
    }
    @Override
    @Transactional
    public boolean cancelDeductStock(String productId, int quantity, String transactionId) {
        // 1. 检查预扣减日志是否存在
        StockDeductLog stockDeductLog = stockDeductLogRepository.findByTransactionId(transactionId).orElse(null);
        if (stockDeductLog == null) {
            return true; // 预扣减日志不存在,无需取消 (空回滚)
        }
        // 2. 释放预扣减的库存
        Stock stock = stockRepository.findById(productId).orElse(null);
        if (stock == null || stock.getFrozenStock() < quantity) {
            return true; // 库存不存在或者冻结库存不足,可能是confirm已经执行了  (幂等性)
        }
        stock.setFrozenStock(stock.getFrozenStock() - quantity);
        stockRepository.save(stock);
        // 3. 更新预扣减日志状态为 CANCEL
        stockDeductLog.setStatus("CANCEL");
        stockDeductLogRepository.save(stockDeductLog);
        return true; // 库存取消扣减成功
    }
}
// 库存实体类
@Entity
@Table(name = "stock")
@Data
public class Stock {
    @Id
    private String productId;
    private int stock;
    private int frozenStock;
}
// 库存预扣减日志实体类
@Entity
@Table(name = "stock_deduct_log")
@Data
public class StockDeductLog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String transactionId;
    private String productId;
    private int quantity;
    private String status; // TRY, CONFIRM, CANCEL
}
// 库存Repository
public interface StockRepository extends JpaRepository<Stock, String> {
}
// 库存预扣减日志Repository
public interface StockDeductLogRepository extends JpaRepository<StockDeductLog, Long> {
    Optional<StockDeductLog> findByTransactionId(String transactionId);
}
4.3 分布式事务协调器
这里我们使用一个简单的分布式事务协调器,实际应用中可以使用成熟的分布式事务框架,例如 Seata, Atomikos 等。
@Service
public class TransactionCoordinator {
    @Autowired
    private OrderService orderService;
    public boolean executeTCC(String orderId, String productId, int quantity) {
        try {
            // 1. Try 阶段
            boolean tryResult = orderService.tryCreateOrder(orderId, productId, quantity);
            if (!tryResult) {
                System.err.println("Try 阶段失败,订单ID: " + orderId);
                return false;
            }
            // 2. Confirm 阶段
            boolean confirmResult = orderService.confirmCreateOrder(orderId);
            if (!confirmResult) {
                System.err.println("Confirm 阶段失败,订单ID: " + orderId);
                // Confirm 失败,需要进行Cancel
                cancel(orderId, productId, quantity);
                return false;
            }
            return true; // 事务成功
        } catch (Exception e) {
            System.err.println("事务执行异常,订单ID: " + orderId + ", 异常信息: " + e.getMessage());
            // 出现异常,需要进行Cancel
            cancel(orderId, productId, quantity);
            return false;
        }
    }
    private void cancel(String orderId, String productId, int quantity) {
        // 3. Cancel 阶段
        boolean cancelResult = orderService.cancelCreateOrder(orderId);
        if (!cancelResult) {
           //TODO:  需要记录失败日志,并进行重试或人工干预。这里暂时省略重试逻辑
            System.err.println("Cancel 阶段失败,订单ID: " + orderId);
        }
    }
}
5. 关键代码解释
- Try 阶段: 订单服务创建状态为 "TRY" 的订单,并调用库存服务的 
tryDeductStock方法预扣减库存(冻结库存)。如果任何一步失败,抛出异常,触发本地事务回滚。 - Confirm 阶段: 订单服务将订单状态更新为 "CONFIRM",并异步调用库存服务的 
confirmDeductStock方法,实际扣减库存。 这里将库存confirm操作异步执行,可以提升confirm阶段的效率,避免confirm阶段耗时过长。 - Cancel 阶段: 订单服务调用库存服务的 
cancelDeductStock方法,释放预扣减的库存,并删除订单记录。 - 事务ID: 使用订单ID作为事务ID,传递给库存服务,用于保证幂等性。
 - 日志表: 库存服务使用 
stock_deduct_log表记录库存预扣减的状态,用于在 Confirm 和 Cancel 阶段进行幂等性判断。 
6. 异常情况处理
- Try 阶段失败: 订单服务本地事务回滚,无需执行 Cancel 阶段。
 - Confirm 阶段失败: 需要进行重试,或者人工干预。如果重试失败,需要执行 Cancel 阶段。
 - Cancel 阶段失败: 需要进行重试,或者人工干预。
 - 空回滚: 在 Cancel 阶段,如果订单记录不存在,或者预扣减日志不存在,则直接返回成功,不做任何操作。
 - 悬挂: 在 Confirm 阶段,如果预扣减日志不存在,或者库存不存在,则直接返回成功,不做任何操作。
 
7. TCC模式的优点与缺点
优点:
- 性能较高: 允许在应用层面控制事务,避免了 XA 协议的性能瓶颈。
 - 灵活性高: 可以根据业务场景自定义补偿逻辑。
 - 跨数据库支持: 可以跨不同的数据库和数据源进行事务处理。
 
缺点:
- 开发复杂度高: 需要手动实现 Try, Confirm, Cancel 三个阶段的逻辑,以及幂等性、空回滚、悬挂等问题的处理。
 - 侵入性强: 业务代码需要感知 TCC 模式的存在。
 - 数据一致性延迟: TCC 模式是最终一致性,数据一致性存在一定的延迟。
 
8. 选择合适的分布式事务方案
选择哪种分布式事务方案,取决于具体的业务场景和需求。
- XA 协议: 适用于对一致性要求极高,且性能要求不高的场景。
 - TCC 模式: 适用于对性能有要求,允许最终一致性的场景。
 - 本地消息表: 适用于异步场景,允许更高的延迟。
 - Seata 等分布式事务框架: 提供了一站式的分布式事务解决方案,可以简化开发工作。
 
总之,TCC 模式是一种强大的分布式事务解决方案,但其复杂性也需要仔细考虑。在选择 TCC 模式之前,请务必评估其优缺点,并根据实际情况进行权衡。
9. 总结与思考
TCC模式通过Try、Confirm和Cancel三个阶段实现了分布式事务的最终一致性,它在性能和灵活性方面具有优势,但也增加了开发复杂度。理解其实现原理和各种异常情况的处理是应用TCC模式的关键。