基于 Hmily 实战 TCC 分布式事务之一

大纲

基础概念

多种事务场景

  • 本地事务:一个服务只需要调用一个数据库实例完成数据的增删改查操作


  • 多库事务:一个服务需要调用多个数据库实例完成数据的增删改查操作


  • 跨服务事务:多个服务需要调用同一个数据库实例完成数据的增删改查操作


  • 跨服务事务:多个服务需要调用多个数据库实例完成数据的增删改查操作

TCC 分布式事务的概述

TCC(Try-Confirm-Cancel) 是一种分布式事务模式,它将一个大的事务拆分为两个阶段(Try 阶段与 Confirm / Cancel 阶段),并由多个相互独立的子事务组成。第一阶段(Try)用于预留 / 冻结资源,需要同步完成;第二阶段(Confirm / Cancel)用于提交或回滚资源,可以异步执行而不影响整体事务结果的一致性。特别注意的是,在极端情况下,TCC 可能需要人工介入处理,例如 Confirm 或 Cancel 一直执行失败或超过最大重试次数时;若处理不当,可能会导致系统出现不一致问题。因此,在对一致性要求极高的金融系统中,通常需要建立独立、完善的事务补偿、监控与人工干预机制,以保障系统的一致性与可恢复性。

TCC(Try Confirm Cancel)属于两阶段补偿型分布式事务(又被称为补偿事务),类似 2PC(两阶段提交)的柔性分布式解决方案,属于 2PC 的改良版。

  • TCC 实现分布式事务一共有三个步骤:

    • Try(尝试待执行的业务):这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源。
    • Confirm(确认执行业务):确认执行业务操作,不做任何业务检查,只使用 Try 阶段预留的业务资源。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即只要 Try 成功,Confirm 就一定成功。若 Confirm 真的出错了,需引入 Confirm 重试机制或人工处理。
    • Cancel(取消待执行的业务):取消 Try 阶段预留的业务资源。通常情况下,采用 TCC 则认为 Cancel 阶段是不会出错的。即只要 Try 失败,Cancel 就一定成功。若 Cancel 阶段真的出错了,需引入 Cancel 重试机制或人工处理。
  • 基于 TCC 实现跨银行转账为例:

    • Try 阶段:先冻结两个银行账户中的资金,防止被其他操作使用。
    • Confirm 阶段:执行实际转账,A 银行账户资金扣减,B 银行账户资金增加。
    • Cancel 阶段:若任一操作执行失败,则进行补偿回滚,例如 A 已经扣款但 B 增加资金失败时,就需要将 A 的资金加回去。

Cancel 执行的时机

在 TCC 事务中,只要有任意一个 Try 阶段执行失败,整体事务即被判定为失败,此时需要对所有已成功执行 Try 的分支事务执行 Cancel 以释放已冻结的资源。Cancel 仅在 Try 阶段失败或整体事务被判定失败时触发;一旦进入 Confirm 阶段,事务结果已经确定(因为所有 Try 执行成功),即使 Confirm 执行失败,也只能通过重试 Confirm 来完成事务,而不应再执行 Cancel。简而言之,Cancel 是 Try 的反向操作,不是 Confirm 的补偿操作。

  • TCC 事务的优点:

    • 将数据库层的两阶段提交上提到了应用层来实现,规避了数据库层的 2PC 性能低下的问题
    • 可以严格保证事务的最终一致性,尤其适合对最终一致性要求极高的核心业务场景(如支付、交易、资金结算等),并且通常要求各个步骤的执行时间较短
  • TCC 事务的缺点:

    • TCC 的 Try、Confirm 和 Cancel 操作功能需要由业务提供,开发成本高
    • 严重依赖人工编写的补偿逻辑,导致业务代码庞杂、难以维护,因此在实际项目中应用相对较少
  • TCC 事务的适用场景:

    • 对最终一致性要求极高、允许短暂中间状态、但希望不一致窗口尽可能小的核心业务场景,如金融转账、交易、支付结算等
    • 业务链路相对较短、各个业务步骤执行时间可控,能够在较短时间内完成 Try / Confirm / Cancel 的场景
    • 系统能够接受较高的开发与维护成本,且团队具备实现完善的幂等控制、补偿逻辑和异常兜底能力的情况下
  • TCC 两阶段提交与 2PC / XA 的区别:

    • 2PC / XA 是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,会一直持有资源的锁
    • TCC 是业务层面的分布式事务,满足最终一致性,不会一直持有资源的锁
  • 为什么 TCC 属于最终一致性方案,但可以用于如金融转账、交易、支付等场景:

    • 金融转账、交易、支付系统真正要求的是 “结果零容错”,而不是 “过程零中间状态”。只要中间状态对用户是可理解、可控且可恢复的,系统就能够满足金融级一致性要求。TCC 通过冻结资源与快速补偿机制,将不一致性严格限制在系统内部,并通过业务语义对外提供一致的用户视图,因此在不依赖强一致性事务的前提下,仍然能够保证最终结果绝对正确。
    • 从本质上看,TCC 是一种 “用业务语义模拟强一致性体验” 的最终一致性方案。以银行转账为例,真实的金融系统通常会维护多种账户余额维度(如可用余额、冻结余额、在途金额、已清算 / 未清算金额),总账始终守恒,对用户展示的是业务一致视图。在 TCC 中,Try 阶段只是冻结余额,并不改变最终账务归属;Confirm 阶段才真正完成扣减;Cancel 阶段则会完整回滚冻结。因此,用户看到的是 “处理中” 或 “冻结” 状态,而不是资金凭空消失或异常增加。
    • 在工程实践上,TCC 通过冻结而非直接扣减、同步编排而非纯异步、秒级补偿而非长时间不一致,实现了高可用、可解释且账务绝对正确的交易流程。相比之下,2PC / XA 在金融高并发场景下往往存在资源锁定时间长、对数据库和中间件侵入强、协调者异常容易导致长时间阻塞等问题,极易放大故障风险。而金融系统真正忌讳的是卡死、雪崩和不可恢复的阻塞,而不是短暂且可控的 “处理中” 状态。

提示

分布式系统需要遵循一致性相关的 CAP 理论与 BASE 理论,TCC 分布式事务并不违背这些理论。分布式系统中的最终一致性原则同样适用于 TCC。TCC 通过第二阶段(Confirm / Cancel)的补偿机制,在保证业务语义正确的前提下实现最终一致性。从工程实践角度看,所有分布式事务解决方案都必须具备补偿机制,因此本质上都属于最终一致性方案。在选择具体的分布式事务解决方案时,需要重点评估补偿机制的实现复杂度与执行效率。

特别注意

TCC 属于最终一致性方案,而不是强一致性方案。TCC 适合的不是 "必须强一致" 的场景,而是 "结果绝不能错,但过程可以短暂不一致" 的场景。虽然 TCC 通过 Try-Confirm-Cancel 的同步编排流程,在 Try 阶段预留资源,并在任一参与方失败时立即进入 Cancel,通常可以在秒级内完成一致性恢复,从效果上看 TCC 被认为是最终一致性中 "最接近强一致性" 的方案;但其一致性仍依赖业务层的补偿、重试和幂等机制,允许中间状态短暂存在,无法提供真正意义上的原子提交和瞬时一致性。因此 "接近强一致性" 并不等同于 "强一致性",TCC 其本质仍然是最终一致性解决方案。

TCC 分布式事务的架构

  • TCC 分布式事务的整体架构图如下:

TCC 分布式事务的应用

TCC 在多库事务中的应用

TCC 在跨服务事务中的应用


TCC 分布式事务的性能优化

账户系统 TCC 事务实战(理论)

账户系统的整体架构

小型账户系统


亿级账户系统

体系内部转账


内部转账给外部


外部转账给内部


账户系统的 TCC 模型

TCC 模型之一(错误)

TCC 模型之二(错误)

TCC 模型之三(错误)

TCC 模型之四(正确)

TCC 模型之五(正确)

TCC 模型总结说明

账户系统的 TCC 事务异常分析

本节将基于以下账户系统 TCC 模型,对 TCC 分布式事务在不同异常场景下的处理方式进行分析。

第一阶段异常

第二阶段提交异常

第二阶段回滚异常

本地事务与 TCC 事务发生冲突

第二阶段的 Confirm 是否允许抛出异常

第二阶段的 Cancel 是否允许抛出异常

空回滚(Empty Rollback)

第二阶段重复提交

防悬挂(Anti Suspension)

防悬挂(Anti Suspension)与空回滚(Emptry Rollback)的区别

  • 防悬挂的概念

    • 防悬挂是指防止 Try 请求在事务已经进入结束状态之后迟到执行的问题。
    • 当全局事务已经 Confirm(提交完成)或 Cancel(回滚完成)时,说明该分支事务生命周期已结束;此时如果 Try 请求因网络延迟、RPC 重试等原因再次到达,必须直接拒绝执行,否则会导致资源被再次冻结却再也得不到释放,形成 “悬挂资源”。
  • 空回滚的概念

    • 空回滚是指在 Try 实际从未成功执行过的情况下,由于 Try 超时、网络异常或事务协调器直接判定事务失败,Cancel 被调用的问题。
    • 此时 Cancel 并不是对已执行 Try 的补偿,而是对一笔 “空事务” 的回滚,实现上必须先判断 Try 是否执行过;若 Try 未执行过,Cancel 只能记录回滚状态而不做任何资源释放操作,以避免对未冻结的资源进行错误回滚。
  • 两者的核心区别

    • 关注点不同
      • 防悬挂:关注 Try 是否在事务已经结束(Confirm / Cancel)之后仍被执行
      • 空回滚:关注 Cancel 是否在 Try 实际未执行过或未成功执行过的情况下被调用
    • 触发阶段不同
      • 防悬挂:发生在 Try 阶段
      • 空回滚:发生在 Cancel 阶段
    • 解决的问题不同
      • 防悬挂:防止 “迟到的 Try” 导致资源悬挂
      • 空回滚:防止 “多余的 Cancel” 破坏业务数据

账户系统的 TCC 事务隔离级别

事务隔离级别的基础概念

事务表示多个数据操作组成一个完整的事务单元,在这个事务内的所有数据操作要么同时成功,要么同时失败。数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为 隔离级别SQL 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但数据库的并发性能越差。

提示

事务隔离级别是数据库用来规定并发事务之间相互可见性和相互影响程度的一套规则,它通过在性能与一致性之间做取舍,明确一个事务在执行过程中能否看到其他未提交或已提交事务的数据变化,从而避免或容忍脏读、不可重复读和幻读等数据库事务并发问题。

为了解决事务并发问题(脏读、不可重复读、幻读),主流的关系型数据库都会提供以下四种事务隔离级别。

  • (1) 读未提交(Read Uncommitted)
    • 在该隔离级别,所有事务都可以看到其他未提交事务所做的改变
    • 该隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。
    • 该隔离级别只能解决第一类更新丢失问题,不能解决脏读、不可重复读、幻读的问题。
  • (2) 读已提交(Read Committed)
    • 这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的),例如 Oracle 数据库。
    • 该隔离级别满足了隔离的简单定义:一个事务只能看见已提交事务所做的改变
    • 该隔离级别可以解决脏读问题,但会出现不可重复读、幻读问题。
  • (3) 可重复读(Repeatable Read)
    • 这是 MySQL 的默认事务隔离级别,它可以确保在整个事务过程中,对同一条数据的读取结果是相同的,不管其他事务是否在对共享数据进行更新,也不管其他事务更新提交与否
    • 该隔离级别可以解决脏读、不可重复读的问题,但会出现幻读问题。
  • (4) 串行化(Serializable)
    • 这是最高的事务隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决脏读、不可重复读、幻读、第一类更新丢失、第二类更新丢失问题
    • 该隔离级别可以解决所有的事务并发问题,但可能导致大量的超时现象和锁竞争,通常数据库不会用这个隔离级别,可用其他的方案来解决这些问题,例如乐观锁和悲观锁。

上述四种事务隔离级别会产生的并发问题如下(YES 表示存在对应的问题,NO 表示不存在对应的问题):

各种关系型数据库对事务隔离级别的支持程度如下(YES 表示支持,NO 表示不支持):

OracleMySQL
Read UncommittedNOYES
Read CommittedYES(默认)YES
Repeatable ReadNOYES(默认)
SerializableYESYES

提示

  • 在 Spring 框架中,事务隔离级别可以通过 @Transactional 注解中的 isolation 属性定义。
  • MySQL 的默认隔离级别是可重复读(Repeatable Read),而 Oracle 的默认隔离级别是读已提交(Read Committed)。

TCC 事务隔离级别的推演

参考资料