架构成长指南

  • 首页
  • prometheus
  • 云原生
  • 关于
不积跬步,无以至千里
  1. 首页
  2. 分布式事务
  3. 正文

分布式事务解决方案:深入理解TCC模式

2024年12月6日 1434点热度 0人点赞 0条评论

引言

在分布式系统中,事务处理一直是一个复杂的话题。想象一下,当你在网上商城购物时,整个过程涉及:

  • 订单系统创建订单
  • 库存系统扣减库存
  • 支付系统完成支付
  • 积分系统增加积分

这些操作分布在不同的服务中,如何保证它们要么全部成功,要么全部失败?这就是分布式事务需要解决的问题。

分布式事务的挑战

传统事务的局限

在单体应用中,我们习惯使用数据库的 ACID 事务:

@Transactional
public void createOrder(Order order) {
    // 创建订单
    orderRepository.save(order);
    // 扣减库存
    inventoryRepository.deduct(order.getProductId(), order.getQuantity());
    // 扣减余额
    accountRepository.deduct(order.getUserId(), order.getAmount());
}

但在分布式环境下,这种方式行不通了,因为:

  1. 跨多个数据库
  2. 跨多个服务
  3. 网络可能失败
  4. 服务可能宕机

CAP 理论的限制

在分布式系统中,我们不得不在以下三个特性中做出选择:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容错性(Partition tolerance)

分布式事务解决方案:深入理解TCC模式插图

TCC 模式介绍

什么是 TCC?

TCC(Try-Confirm-Cancel)是一种补偿性事务模式,它将一个完整的业务操作分为二步完成:

  1. Try: 尝试执行业务

    • 完成所有业务检查
    • 预留必要的业务资源
  2. Confirm: 确认执行业务

    • 真正执行业务
    • 不做任何业务检查
    • 只使用 Try 阶段预留的资源
  3. Cancel: 取消执行业务

    • 释放 Try 阶段预留的资源
    • 回滚操作

分布式事务解决方案:深入理解TCC模式插图1

来源:seata

TCC 示例:订单支付流程

让我们通过一个具体的订单支付场景来理解 TCC:

// 订单服务的 TCC 实现
public class OrderTccService {

    // Try: 创建预订单
    @Transactional
    public void tryCreate(Order order) {
        // 检查订单参数
        validateOrder(order);
        // 创建预订单
        order.setStatus(OrderStatus.TRYING);
        orderRepository.save(order);
    }

    // Confirm: 确认订单
    @Transactional
    public void confirmCreate(String orderId) {
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CONFIRMED);
        orderRepository.save(order);
    }

    // Cancel: 取消订单
    @Transactional
    public void cancelCreate(String orderId) {
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

// 库存服务的 TCC 实现
public class InventoryTccService {

    // Try: 冻结库存
    @Transactional
    public void tryDeduct(String productId, int quantity) {
        Inventory inventory = inventoryRepository.findById(productId);
        // 检查并冻结库存
        if (inventory.getAvailable() < quantity) {
            throw new InsufficientInventoryException();
        }
        inventory.setFrozen(inventory.getFrozen() + quantity);
        inventory.setAvailable(inventory.getAvailable() - quantity);
        inventoryRepository.save(inventory);
    }

    // Confirm: 确认扣减
    @Transactional
    public void confirmDeduct(String productId, int quantity) {
        Inventory inventory = inventoryRepository.findById(productId);
        inventory.setFrozen(inventory.getFrozen() - quantity);
        inventoryRepository.save(inventory);
    }

    // Cancel: 解冻库存
    @Transactional
    public void cancelDeduct(String productId, int quantity) {
        Inventory inventory = inventoryRepository.findById(productId);
        inventory.setFrozen(inventory.getFrozen() - quantity);
        inventory.setAvailable(inventory.getAvailable() + quantity);
        inventoryRepository.save(inventory);
    }
}

// 支付服务的 TCC 实现
public class PaymentTccService {

    // Try: 冻结金额
    @Transactional
    public void tryDeduct(String userId, BigDecimal amount) {
        Account account = accountRepository.findById(userId);
        // 检查并冻结金额
        if (account.getAvailable().compareTo(amount) < 0) {
            throw new InsufficientBalanceException();
        }
        account.setFrozen(account.getFrozen().add(amount));
        account.setAvailable(account.getAvailable().subtract(amount));
        accountRepository.save(account);
    }

    // Confirm: 确认支付
    @Transactional
    public void confirmDeduct(String userId, BigDecimal amount) {
        Account account = accountRepository.findById(userId);
        account.setFrozen(account.getFrozen().subtract(amount));
        accountRepository.save(account);
    }

    // Cancel: 解冻金额
    @Transactional
    public void cancelDeduct(String userId, BigDecimal amount) {
        Account account = accountRepository.findById(userId);
        account.setFrozen(account.getFrozen().subtract(amount));
        account.setAvailable(account.getAvailable().add(amount));
        accountRepository.save(account);
    }
}

TCC 事务协调器

为了协调整个 TCC 流程,我们需要一个事务协调器:

@Service
public class OrderTccCoordinator {

    @Autowired
    private OrderTccService orderService;

    @Autowired
    private InventoryTccService inventoryService;

    @Autowired
    private PaymentTccService paymentService;

    public void createOrder(Order order) {
        String xid = generateTransactionId();

        try {
            // ==== Try 阶段 ====
            // 1. 创建预订单
            orderService.tryCreate(order);

            // 2. 尝试扣减库存
            inventoryService.tryDeduct(
                order.getProductId(), 
                order.getQuantity()
            );

            // 3. 尝试扣减余额
            paymentService.tryDeduct(
                order.getUserId(), 
                order.getAmount()
            );

            // ==== Confirm 阶段 ====
            // 1. 确认订单
            orderService.confirmCreate(order.getId());

            // 2. 确认库存扣减
            inventoryService.confirmDeduct(
                order.getProductId(), 
                order.getQuantity()
            );

            // 3. 确认支付
            paymentService.confirmDeduct(
                order.getUserId(), 
                order.getAmount()
            );

        } catch (Exception e) {
            // ==== Cancel 阶段 ====
            // 1. 取消订单
            orderService.cancelCreate(order.getId());

            // 2. 恢复库存
            inventoryService.cancelDeduct(
                order.getProductId(), 
                order.getQuantity()
            );

            // 3. 恢复余额
            paymentService.cancelDeduct(
                order.getUserId(), 
                order.getAmount()
            );

            throw new OrderCreateFailedException(e);
        }
    }
}

TCC 实现要点

1. 业务模型设计

在实现 TCC 时,业务模型需要考虑预留资源的状态:

public class Inventory {
    private String productId;
    private int total;      // 总库存
    private int available;  // 可用库存
    private int frozen;     // 冻结库存
}

public class Account {
    private String userId;
    private BigDecimal total;     // 总额
    private BigDecimal available; // 可用余额
    private BigDecimal frozen;    // 冻结金额
}

分布式事务解决方案:深入理解TCC模式插图2

图 3: TCC 中的资源状态变化,来源 seata

2. 幂等性设计

所有操作都需要保证幂等,因为在网络异常时可能会重试:

@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
    // 检查是否已经执行过
    if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
        return;
    }

    // 执行业务逻辑
    Account account = accountRepository.findById(userId);
    account.setFrozen(account.getFrozen().add(amount));
    account.setAvailable(account.getAvailable().subtract(amount));
    accountRepository.save(account);

    // 记录执行日志
    tccLogRepository.save(new TccLog(xid, "try"));
}

3. 防悬挂设计

为什么需要防悬挂?

在分布式系统中,网络延迟、服务故障等原因可能导致一个奇怪的现象,Cancel 操作比 Try 操作先执行。这就是所谓的"悬挂"问题。具体场景如下:

事务管理器在调用 TCC 服务的一阶段 Try 操作时事务时,由于网络拥堵,Try 请求没有及时到达,事务管理器超时后,发起了 Cancel 请求完成后,此时原来的 Try 请求才到达,如果在执行这个延迟的 Try 请求,将导致资源被错误锁定

分布式事务解决方案:深入理解TCC模式插图3

*图: TCC 悬挂问题示意图,来源:seata

解决方案

核心思路是记录每个事务的执行状态,并在执行 Try 操作前进行检查:

@Service
public class TccTransactionService {

    @Autowired
    private TccLogRepository tccLogRepository;

    @Transactional
    public void tryDeduct(String userId, BigDecimal amount, String xid) {
        // 1. 检查是否已经被 Cancel
        if (tccLogRepository.existsByXidAndPhase(xid, "cancel")) {
            throw new TransactionCancelledException("Transaction already cancelled");
        }

        // 2. 检查是否已经执行过 Try (幂等性检查)
        if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
            return;
        }

        // 3. 执行业务逻辑
        Account account = accountRepository.findById(userId);
        if (account.getAvailable().compareTo(amount) < 0) {
            throw new InsufficientBalanceException();
        }

        // 4. 记录执行日志
        account.setFrozen(account.getFrozen().add(amount));
        account.setAvailable(account.getAvailable().subtract(amount));
        accountRepository.save(account);
        tccLogRepository.save(new TccLog(xid, "try"));
    }
}

4. 超时处理

为什么需要超时处理?

在分布式环境下,超时是不可避免的,可能由于以下原因导致:

  • 网络延迟或故障
  • 服务器负载过高
  • 服务进程崩溃
  • 死锁

如果不处理超时,会造成严重后果:

  • 资源被无限期锁定
  • 事务无法正常结束
  • 系统可用性降低
  • 用户体验变差

超时处理机制

  1. 定时扫描超时事务
@Component
public class TccTimeoutChecker {

    @Autowired
    private TccLogRepository tccLogRepository;

    @Autowired
    private TccTransactionHandler transactionHandler;

    @Scheduled(fixedRate = 60000)  // 每分钟执行一次
    public void checkTimeout() {
        // 1. 查找超时的事务
        List<TccLog> timeoutLogs = tccLogRepository
            .findByPhaseAndCreateTimeBefore(
                "try", 
                LocalDateTime.now().minusMinutes(5)
            );

        for (TccLog log : timeoutLogs) {
            try {
                // 2. 执行 Cancel 操作
                transactionHandler.cancelTransaction(log.getXid());

                // 3. 记录取消日志
                log.setPhase("cancel");
                log.setUpdateTime(LocalDateTime.now());
                tccLogRepository.save(log);

            } catch (Exception e) {
                // 4. 记录错误,可能需要人工介入
                errorLogger.log(
                    "Failed to cancel timeout transaction: " + log.getXid(),
                    e
                );
            }
        }
    }
}
  1. 超时配置管理
@Configuration
public class TccConfig {

    @Value("${tcc.transaction.timeout:60000}")
    private long transactionTimeout;  // 默认60秒

    @Value("${tcc.check.interval:5000}")
    private long checkInterval;       // 默认5秒

    @Value("${tcc.retry.max:3}")
    private int maxRetryCount;        // 默认重试3次

    @Value("${tcc.retry.interval:1000}")
    private long retryInterval;       // 默认重试间隔1秒

    // getter and setter
}
  1. 监控和告警
@Component
public class TccMonitor {

    @Autowired
    private AlertService alertService;

    public void onTransactionTimeout(String xid) {
        // 记录监控指标
        MetricsRegistry.counter("tcc.timeout").increment();

        // 发送告警
        alertService.sendAlert(
            "TCC Transaction Timeout",
            String.format("Transaction %s timeout", xid),
            AlertLevel.WARNING
        );
    }

    public void onCancelFailed(String xid, Exception e) {
        // 记录监控指标
        MetricsRegistry.counter("tcc.cancel.failed").increment();

        // 发送告警
        alertService.sendAlert(
            "TCC Cancel Failed",
            String.format("Transaction %s cancel failed: %s", xid, e.getMessage()),
            AlertLevel.ERROR
        );
    }
}

最佳实践

  1. 超时时间设置

    • 根据业务特点设置合理的超时时间
    • 考虑网络延迟和服务响应时间
    • 为复杂业务预留足够的处理时间
    • 不同类型的事务可以设置不同的超时时间
  2. 重试机制

    • 实现指数退避算法
    • 设置最大重试次数
    • 合理的重试间隔
    • 重试时要考虑幂等性
  3. 监控和告警

    • 监控超时事务数量
    • 监控 Cancel 操作的成功率
    • 监控资源占用情况
    • 设置合理的告警阈值
  4. 人工干预

    • 提供管理后台
    • 支持手动触发 Cancel
    • 提供事务状态查询
    • 记录详细的操作日志

通过这些机制的组合,我们可以构建一个健壮的 TCC 事务处理系统,能够:

  • 及时发现并处理超时事务
  • 防止资源被长期锁定
  • 提供完善的监控和运维能力
  • 在出现问题时及时告警并支持人工介入

最佳实践

  1. 资源预留

    • Try 阶段要预留足够的资源
    • 预留资源要考虑并发情况
    • 预留时间要合理设置
  2. 状态机制

    • 明确定义每个阶段的状态
    • 状态转换要有清晰的规则
    • 保存状态转换历史
  3. 异常处理

    • 所有异常都要有补偿措施
    • 补偿操作要能重试
    • 重试策略要合理设置
  4. 监控告警

    • 监控每个阶段的执行情况
    • 设置合理的告警阈值
    • 提供人工干预的接口

适用场景

TCC 模式适合:

  1. 强一致性要求高的业务
  2. 实时性要求高的场景
  3. 有资源锁定需求的操作

不适合:

  1. 业务逻辑简单的场景
  2. 对性能要求特别高的场景
  3. 补偿成本过高的业务

结论

TCC 是一种强大的分布式事务解决方案,它通过巧妙的补偿机制来保证事务的一致性。虽然实现较为复杂,但在某些场景下是不可替代的选择。

关键是要:

  • 理解业务场景
  • 合理设计补偿逻辑
  • 做好异常处理
  • 重视监控告警

通过合理使用 TCC 模式,我们可以在分布式系统中实现可靠的事务处理。

累计浏览量: 1,467
标签: 暂无
最后更新:2024年12月6日

蜗牛

大家好,我是蜗牛哥,工作11年的老司机,目前在某头部跨国外企担任基础架构负责人,除了正常工作外,写写技术文章把遇到的一些坑和一些经验分享出来,主要语言JAVA,擅长基础架构与中间件开发,目前主攻方向云原生

点赞

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复
我的公众号
文章目录
  • 引言
  • 分布式事务的挑战
    • 传统事务的局限
    • CAP 理论的限制
  • TCC 模式介绍
    • 什么是 TCC?
    • TCC 示例:订单支付流程
    • TCC 事务协调器
  • TCC 实现要点
    • 1. 业务模型设计
    • 2. 幂等性设计
    • 3. 防悬挂设计
    • 4. 超时处理
  • 最佳实践
  • 适用场景
  • 结论
标签聚合
分布式 k8s 大数据 java 云原生 可观测性 loki prometheus
浏览最多的文章
  • 关于 (9,905)
  • 10 分钟在K8s中部署轻量级日志系统 Loki (2,224)
  • 什么是 doris,为什么几乎国内大厂都会使用它? (2,104)
  • mysql for update是锁表还是锁行 (2,079)
  • 从零开始:使用Prometheus与Grafana搭建监控系统 (2,036)

COPYRIGHT © 2023 架构成长指南. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

蜀ICP备2023043681号-1