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

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

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

  • 订单系统创建订单

  • 库存系统扣减库存

  • 支付系统完成支付

  • 积分系统增加积分

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

060d2a240ef12fc41d7317b420313ca3ade4fb.png

一、分布式事务的挑战

1.传统事务的局限

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

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

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

  • 跨多个数据库

  • 跨多个服务

  • 网络可能失败

  • 服务可能宕机

2.CAP 理论的限制

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

  • 一致性(Consistency)

  • 可用性(Availability)

  • 分区容错性(Partition tolerance)

867e266243c55e3162d375eb486c76da00c61c.webp

二、TCC 模式介绍

1.什么是 TCC?

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

(1) Try: 尝试执行业务

  • 完成所有业务检查

  • 预留必要的业务资源

(2) Confirm: 确认执行业务

  • 真正执行业务

  • 不做任何业务检查

  • 只使用 Try 阶段预留的资源

(3) Cancel: 取消执行业务

  • 释放 Try 阶段预留的资源

  • 回滚操作

924af598335c36ab194752fe0c0482a40e511b.webp

来源:seata

2.TCC 示例:订单支付流程

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

// 订单服务的 TCC 实现publicclassOrderTccService{// Try: 创建预订单@TransactionalpublicvoidtryCreate(Order order){// 检查订单参数validateOrder(order);// 创建预订单order.setStatus(OrderStatus.TRYING);orderRepository.save(order);}// Confirm: 确认订单@TransactionalpublicvoidconfirmCreate(String orderId){Order order=orderRepository.findById(orderId);order.setStatus(OrderStatus.CONFIRMED);orderRepository.save(order);}// Cancel: 取消订单@TransactionalpublicvoidcancelCreate(String orderId){Order order=orderRepository.findById(orderId);order.setStatus(OrderStatus.CANCELLED);orderRepository.save(order);}}// 库存服务的 TCC 实现publicclassInventoryTccService{// Try: 冻结库存@TransactionalpublicvoidtryDeduct(String productId,int quantity){Inventory inventory=inventoryRepository.findById(productId);// 检查并冻结库存if(inventory.getAvailable()<quantity){thrownewInsufficientInventoryException();}inventory.setFrozen(inventory.getFrozen()+quantity);inventory.setAvailable(inventory.getAvailable()-quantity);inventoryRepository.save(inventory);}// Confirm: 确认扣减@TransactionalpublicvoidconfirmDeduct(String productId,int quantity){Inventory inventory=inventoryRepository.findById(productId);inventory.setFrozen(inventory.getFrozen()-quantity);inventoryRepository.save(inventory);}// Cancel: 解冻库存@TransactionalpublicvoidcancelDeduct(String productId,int quantity){Inventory inventory=inventoryRepository.findById(productId);inventory.setFrozen(inventory.getFrozen()-quantity);inventory.setAvailable(inventory.getAvailable()+quantity);inventoryRepository.save(inventory);}}// 支付服务的 TCC 实现publicclassPaymentTccService{// Try: 冻结金额@TransactionalpublicvoidtryDeduct(String userId,BigDecimal amount){Account account=accountRepository.findById(userId);// 检查并冻结金额if(account.getAvailable().compareTo(amount)<0){thrownewInsufficientBalanceException();}account.setFrozen(account.getFrozen().add(amount));account.setAvailable(account.getAvailable().subtract(amount));accountRepository.save(account);}// Confirm: 确认支付@TransactionalpublicvoidconfirmDeduct(String userId,BigDecimal amount){Account account=accountRepository.findById(userId);account.setFrozen(account.getFrozen().subtract(amount));accountRepository.save(account);}// Cancel: 解冻金额@TransactionalpublicvoidcancelDeduct(String userId,BigDecimal amount){Account account=accountRepository.findById(userId);account.setFrozen(account.getFrozen().subtract(amount));account.setAvailable(account.getAvailable().add(amount));accountRepository.save(account);}}

3.TCC 事务协调器

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

@ServicepublicclassOrderTccCoordinator{@AutowiredprivateOrderTccService orderService;@AutowiredprivateInventoryTccService inventoryService;@AutowiredprivatePaymentTccService paymentService;publicvoidcreateOrder(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());thrownewOrderCreateFailedException(e);}}}

三、TCC 实现要点

1. 业务模型设计

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

publicclassInventory{privateString productId;privateint total;// 总库存privateint available;// 可用库存privateint frozen;// 冻结库存}publicclassAccount{privateString userId;privateBigDecimal total;// 总额privateBigDecimal available;// 可用余额privateBigDecimal frozen;// 冻结金额}

71e75c630bce5cc744e0310cce2fa7357205f2.webp

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

2. 幂等性设计

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

@TransactionalpublicvoidtryDeduct(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.s

3. 防悬挂设计

(1) 为什么需要防悬挂?

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

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

c6e932f394862097122339e9e7050fd9a317c5.webp

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

(2) 解决方案

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

@ServicepublicclassTccTransactionService{@AutowiredprivateTccLogRepository tccLogRepository;@TransactionalpublicvoidtryDeduct(String userId,BigDecimal amount,String xid){// 1. 检查是否已经被 Cancelif(tccLogRepository.existsByXidAndPhase(xid,"cancel")){thrownewTransactionCancelledException("Transaction already cancelled");}// 2. 检查是否已经执行过 Try (幂等性检查)if(tccLogRepository.existsByXidAndPhase(xid,"try")){return;}// 3. 执行业务逻辑Account account=accountRepository.findById(userId);if(account.getAvailable().compareTo(amount)<0){thrownewInsufficientBalanceException();}// 4. 记录执行日志account.setFrozen(account.getFrozen().add(amount));account.setAvailable(account.getAvailable().subtract(amount));accountRepository.save(account);tccLogRepository.save(newTccLog(xid,"try"));}}

4. 超时处理

(1) 为什么需要超时处理?

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

  • 网络延迟或故障

  • 服务器负载过高

  • 服务进程崩溃

  • 死锁

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

  • 资源被无限期锁定

  • 事务无法正常结束

  • 系统可用性降低

  • 用户体验变差

(2) 超时处理机制

定时扫描超时事务:

@ComponentpublicclassTccTimeoutChecker{@AutowiredprivateTccLogRepository tccLogRepository;@AutowiredprivateTccTransactionHandler transactionHandler;@Scheduled(fixedRate=60000)// 每分钟执行一次publicvoidcheckTimeout(){// 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);}}}}

超时配置管理:

@ConfigurationpublicclassTccConfig{@Value("${tcc.transaction.timeout:60000}")privatelong transactionTimeout;// 默认60秒@Value("${tcc.check.interval:5000}")privatelong checkInterval;// 默认5秒@Value("${tcc.retry.max:3}")privateint maxRetryCount;// 默认重试3次@Value("${tcc.retry.interval:1000}")privatelong retryInterval;// 默认重试间隔1秒// getter and setter}

监控和告警:

@ComponentpublicclassTccMonitor{@AutowiredprivateAlertService alertService;publicvoidonTransactionTimeout(String xid){// 记录监控指标MetricsRegistry.counter("tcc.timeout").increment();// 发送告警alertService.sendAlert("TCC Transaction Timeout",String.format("Transaction %s timeout",xid),AlertLevel.WARNING);}publicvoidonCancelFailed(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);}}

(3) 最佳实践

超时时间设置:

  • 根据业务特点设置合理的超时时间

  • 考虑网络延迟和服务响应时间

  • 为复杂业务预留足够的处理时间

  • 不同类型的事务可以设置不同的超时时间

重试机制:

  • 实现指数退避算法

  • 设置最大重试次数

  • 合理的重试间隔

  • 重试时要考虑幂等性

监控和告警:

  • 监控超时事务数量

  • 监控 Cancel 操作的成功率

  • 监控资源占用情况

  • 设置合理的告警阈值

人工干预:

  • 提供管理后台

  • 支持手动触发 Cancel

  • 提供事务状态查询

  • 记录详细的操作日志

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

  • 及时发现并处理超时事务

  • 防止资源被长期锁定

  • 提供完善的监控和运维能力

  • 在出现问题时及时告警并支持人工介入

四、最佳实践

资源预留:

  • Try 阶段要预留足够的资源

  • 预留资源要考虑并发情况

  • 预留时间要合理设置

状态机制:

  • 明确定义每个阶段的状态

  • 状态转换要有清晰的规则

  • 保存状态转换历史

异常处理:

  • 所有异常都要有补偿措施

  • 补偿操作要能重试

  • 重试策略要合理设置

监控告警:

  • 监控每个阶段的执行情况

  • 设置合理的告警阈值

  • 提供人工干预的接口

五、适用场景

TCC 模式适合:

  • 强一致性要求高的业务

  • 实时性要求高的场景

  • 有资源锁定需求的操作

不适合:

  • 业务逻辑简单的场景

  • 对性能要求特别高的场景

  • 补偿成本过高的业务

六、结论

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

关键是要:

  • 理解业务场景

  • 合理设计补偿逻辑

  • 做好异常处理

  • 重视监控告警

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

50    2024-12-09 09:35:00    TCC 分布式事务