TCC 分布式事务详解:构建可靠的分布式事务解决方案

222 阅读8分钟

​ 在分布式系统中,跨服务的事务一致性一直是个难题。本文将结合具体业务场景,详细解析 TCC 分布式事务的实现原理、流程以及优缺点,帮助你彻底搞懂这一常用的分布式事务解决方案。

一、业务场景:为什么需要 TCC?

我们以一个常见的电商订单流程为例,用户支付订单后,系统需要完成以下操作:

  1. 订单服务:修改订单状态为 “已支付”
  2. 库存服务:扣减对应商品的库存
  3. 积分服务:为会员增加相应积分
  4. 仓储服务:创建销售出库单

​编辑

这些操作必须作为一个整体事务存在 ——要么全部成功,要么全部失败。否则就会出现数据不一致的问题,比如:

  • 订单状态已改为 “已支付”,但库存服务扣减库存失败,导致库存数量未变
  • 积分成功增加,但仓储服务未创建出库单,导致商品无法发货

​编辑

这种情况下,就需要 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。当任何一个服务的操作失败时,所有已完成的操作都需要回滚。

​编辑

二、TCC 的三个阶段

TCC 是 Try-Confirm-Cancel 的缩写,它将分布式事务的执行过程分为三个阶段,通过这三个阶段的协调来保证事务的一致性。

(一)阶段一:Try(尝试阶段)

在 Try 阶段,各个服务并不会直接完成业务操作,而是进行一些预备操作,主要目的是:

  1. 检查业务所需的资源是否可用
  2. 对业务所需的资源进行锁定或预留

以我们的订单流程为例,各个服务在 Try 阶段的操作如下:

订单服务
不直接将订单状态修改为 “已支付”,而是先修改为 “UPDATING”(中间状态),表示正在处理中。

库存服务
不直接扣减库存,而是将可销售库存减少,同时在冻结库存字段中记录相应数量。例如,库存 100 件,需要扣减 2 件,则可销售库存变为 98,冻结库存变为 2。

积分服务
不直接增加会员积分,而是在预增加积分字段中记录相应数值。例如,会员原有积分 1190,需增加 10 分,则保持积分 1190 不变,预增加积分字段设为 10。

仓储服务
创建销售出库单,但将状态设置为 “UNKNOWN”(未知状态)。

以下是订单服务在 Try 阶段的代码示例:

class OrderService:
    def __init__(self, inv_srv, credit_srv, wms_srv):
        # 库存服务
        self.inv_srv = inv_srv
        # 积分服务
        self.credit_srv = credit_srv
        # 仓储服务
        self.wms_srv = wms_srv

    def try_pay(self):
        # Try阶段:将订单状态修改为中间状态UPDATING
        orm.update_status("UPDATING")
        # 调用库存服务的Try接口
        self.inv_srv.try_reduce_stock()
        # 调用积分服务的Try接口
        self.credit_srv.try_add_credit()
        # 调用仓储服务的Try接口
        self.wms_srv.try_sale_delivery()

​编辑

(二)阶段二:Confirm(确认阶段)

当 TCC 分布式事务框架感知到所有服务的 Try 操作都成功执行后,就会进入 Confirm 阶段。在这个阶段,各个服务会正式执行业务操作,完成最终的数据修改。

各个服务在 Confirm 阶段的操作如下:

订单服务
将订单状态从 “UPDATING” 正式修改为 “TRADE_SUCCESS”(已支付)。

库存服务
将冻结库存字段中的数值清零,完成库存扣减。例如,将冻结的 2 件库存清零,最终可销售库存为 98。

积分服务
将预增加积分字段中的数值添加到实际积分中,并将预增加积分字段清零。例如,将预增加的 10 分添加到原有 1190 分中,变为 1200 分,预增加积分字段变为 0。

仓储服务
将销售出库单的状态从 “UNKNOWN” 修改为 “CREATED”(已创建)。

以下是订单服务在 Confirm 阶段的代码示例:

class OrderServiceConfirm:
    def confirm_pay(self):
        # Confirm阶段:正式将订单状态修改为已支付
        orm.update_status("TRADE_SUCCESS")

​编辑

(三)阶段三:Cancel(取消阶段)

如果在 Try 阶段,任何一个服务的操作失败,TCC 分布式事务框架就会进入 Cancel 阶段。在这个阶段,各个服务会撤销在 Try 阶段所做的预备操作,恢复到事务执行前的状态。

各个服务在 Cancel 阶段的操作如下:

订单服务
将订单状态从 “UPDATING” 修改为 “CANCELED”(已取消)。

库存服务
将冻结库存字段中的数值加回到可销售库存中,并将冻结库存字段清零。例如,将冻结的 2 件库存加回到可销售库存中,可销售库存恢复为 100,冻结库存变为 0。

积分服务
将预增加积分字段中的数值清零,保持原有积分不变。例如,将预增加的 10 分清零,积分仍为 1190。

仓储服务
将销售出库单的状态从 “UNKNOWN” 修改为 “CANCELED”(已取消)。

以下是订单服务在 Cancel 阶段的代码示例:

class OrderServiceCancel:
    def cancel_pay(self):
        # Cancel阶段:将订单状态修改为已取消
        orm.update_status("CANCELED")

​编辑

三、TCC 分布式事务框架的作用

要实现 TCC 分布式事务,必须依赖专门的 TCC 分布式事务框架,如 Java 的 Seata、ByteTCC、Himly、TCC-transaction,Go 语言的 go-seata 等。这些框架主要起到以下作用:

  1. 协调事务流程:感知各个服务 Try 阶段的执行结果,决定进入 Confirm 阶段还是 Cancel 阶段。
  2. 跨服务通信:在不同服务之间传递事务状态信息,调用相应的 Confirm 或 Cancel 接口。
  3. 记录事务日志:将分布式事务的执行过程和状态记录到日志中(可以是磁盘文件或数据库),用于故障恢复。
  4. 重试机制:当 Confirm 或 Cancel 操作执行失败时,框架会根据事务日志进行重试,确保最终执行成功。

四、TCC 的优缺点

优点:

  1. 解决了跨服务的业务操作原子性问题,对于组合支付、订单减库存等场景非常实用。
  2. TCC 将数据库的二阶段提交上升到微服务层面实现,避免了数据库 2 阶段提交中锁冲突导致的长事务低性能问题。
  3. TCC 具有异步高性能的特点,它采用 Try 先检查,然后异步实现 Confirm,真正的提交在 Confirm 方法中完成。

缺点:

  1. 对微服务的侵入性强,每个事务都必须实现 Try、Confirm、Cancel 三个方法,开发成本高,维护和改造的成本也高。
  2. 为了保证事务一致性,Try、Confirm、Cancel 接口必须实现幂等性操作(通常通过定时器 + 重试机制实现)。
  3. 事务管理器需要记录事务日志,会损耗一定性能,同时也会使整个 TCC 事务的执行时间拉长(建议采用 redis 记录事务日志)。
  4. TCC 需要通过锁来确保数据一致性,加锁会导致一定的性能损耗。

五、应对极端情况

  1. 服务重启后的事务恢复
    TCC 事务框架会记录分布式事务的活动日志(可以是磁盘日志文件或数据库记录),保存事务运行的各个阶段和状态。当服务重启后,框架可以根据这些日志恢复未完成的分布式事务。
  2. Confirm 或 Cancel 逻辑执行失败的处理
    TCC 事务框架会通过活动日志记录各个服务的状态。如果发现某个服务的 Cancel 或 Confirm 逻辑执行失败,会不断重试调用,直到成功。
  3. 无法自动解决的问题
    对于一些极小概率的异常情况,如果自动重试无法解决,框架会发送邮件通知工作人员进行人工处理。

六、总结

TCC 分布式事务通过 Try、Confirm、Cancel 三个阶段的协调,实现了跨服务业务操作的原子性。它的核心思想是:

  1. 先通过 Try 操作检查业务可行性并锁定资源
  2. 如果所有 Try 操作都成功,通过 Confirm 操作完成最终业务
  3. 如果任何 Try 操作失败,通过 Cancel 操作回滚所有预备操作

TCC 特别适合那些对数据一致性要求高,且需要跨多个服务协同完成的业务场景。虽然它增加了开发成本和复杂度,但相比其他分布式事务解决方案,它在性能和灵活性方面具有明显优势。

目前,主流的 TCC 框架有 Seata、go-seata 等,在实际项目中可以根据技术栈选择合适的框架来实现 TCC 分布式事务。

如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!