图解分布式事务实现原理(二)

参考

本文参考https://zhuanlan.zhihu.com/p/648556608,在小徐的基础上做了个人的笔记。


TCC 实现方案

TCC 概念简述

TCC(Try-Confirm-Cancel)是一种分布式事务处理模式,旨在保证分布式系统中的事务一致性。它的核心思想是将一个分布式事务分解为多个子事务,每个子事务都有三个关键操作

  • Try:在Try阶段,子事务尝试执行业务检查、资源预留和其他必要的前置操作,以准备好执行实际的业务。如果Try操作成功,表示可以继续执行后续的Confirm操作。

  • Confirm:在Confirm阶段,子事务执行实际的业务确认操作,将之前预留的资源实际分配,完成业务处理。只有当所有分支事务的Try操作都成功,并且TM发起Confirm操作时,才会真正确认事务的执行。

  • Cancel:在Cancel阶段,子事务执行与Try操作相反的操作,即回滚操作,用于撤销之前的资源预留和业务处理。如果Try操作失败,TM将发起所有分支事务的Cancel操作。

**TCC的分布式事务管理器(TM)**首先发起所有分支事务的Try操作,如果有任何一个Try操作失败,TM将会发起所有分支事务的Cancel操作,以确保分布式事务的一致性。只有当所有分支事务的Try操作都成功时,TM才会发起所有分支事务的Confirm操作,最终完成整个事务的确认。

TCC模式通过将分布式事务拆解成多个子事务并明确Try、Confirm和Cancel操作,提供了更细粒度的事务控制,能够适应不同业务场景的需求。然而,实现TCC模式需要开发者谨慎处理异常、重试机制和分布式事务管理,因此具有一定的复杂性

分支事务成功的场景

图解分布式事务实现原理(二)_第1张图片

分支事务失败的场景

图解分布式事务实现原理(二)_第2张图片

TCC 宏观架构

下面是 TCC 分布式事务实现方案的整体架构,大家可以先整体浏览一下存个印象,下面我们会逐一展开介绍:

在 TCC 分布式事务架构中,包含三类角色:

  • 应用方 Application:指的是需要使用到分布式事务能力的应用方,即这套 TCC 框架服务的甲方
  • TCC 组件 TCC Component:指的是需要完成分布式事务中某个特定步骤的子模块. 这个模块通常负责一些状态数据的维护和更新操作,需要对外暴露出 Try、Confirm 和 Cancel 三个 API:
    • Try:锁定资源,通常以类似【冻结】的语义对资源的状态进行描述,保留后续变化的可能性
    • Confirm:对 Try 操作进行二次确认,将记录中的【冻结】态改为【成功】态
    • Cancel:对 Try 操作进行回滚,将记录中的【冻结】状消除或者改为【失败】态. 其底层对应的状态数据会进行回滚
  • 事务协调器 TX Manager:负责统筹分布式事务的执行:
  • 实现 TCC Component 的注册管理功能
  • 负责和 Application 交互,提供分布式事务的创建入口,给予 Application 事务执行结果的响应
  • 串联 Try -> Confirm/Cancel 的两阶段流程. 在第一阶段中批量调用 TCC Component 的 Try 接口,根据其结果,决定第二阶段是批量调用 TCC Component 的 Confirm 接口还是 Cancel 接口

TCC 案例分析

下面我们引入一个具体的分布式事务场景问题,并通过 TCC 架构加以实现,帮助大家进一步提高对 TCC 分布式事务方案的感性认识.

现在假设我们需要维护一个电商后台系统,需要处理来自用户的支付请求. 每当有一笔支付请求到达,我们需要执行下述三步操作,并要求其前后状态保持一致性:

  • 在订单模块中,创建出这笔订单流水记录
  • 在账户模块中,对用户的账户进行相应金额的扣减
  • 在库存模块中,对商品的库存数量进行扣减

上面这三步操作分别需要对接订单、账户、库存三个不同的子模块,底层的状态数据是基于不同的数据库和存储组件实现的,并且我们这套后台系统是基于当前流行的微服务架构实现的,这三子个模块本身对应的就是三个相互独立的微服务,因此如何实现在一笔支付请求处理流程中,使得这三笔操作对应的状态数据始终保持高度一致性,就成了一个非常具有技术挑战性的问题.

图解分布式事务实现原理(二)_第3张图片

首先,我们基于 TCC 的设计理念,将订单模块、账户模块、库存模块分别改造成三个 TCC Component,每个 Component 对应需要暴露出 Try、Confirm、Cancel 三个 API,对应于冻结资源、确认更新资源、回滚解冻资源三个行为.

同时,为了能够简化后续 TX Manager 和 Application 之间的交互协议,每个 TCC Component 会以插件的形式提前注册到 TX Manager 维护的组件市场 Component Market 中,并提前声明好一个全局唯一键与之进行映射关联.


由于每个 TCC Component 需要支持 Try 接口的锁定操作,因此其中维护的数据需要在明细记录中拆出一个用于标识 “冻结” 状态的标签,或者在状态机中拆出一个 “冻结” 状态.

最终在第二阶段的 Confirm 或者 Cancel 请求到达时,再把 ”冻结“ 状态调整为 ”成功“ 或者 ”失败“ 的终态.


下面描述一下,基于 TCC 架构实现后,对应于一次支付请求的分布式事务处理流程:

  1. Application 调用 TX Manager 的接口,创建一轮分布式事务:
  2. Application 需要向 TX Manager 声明,这次操作涉及到的 TCC Component 范围,包括 订单组件、账户组件和库存组件
  3. Application 需要向 TX Manager 提前传递好,用于和每个 TCC Component 交互的请求参数( TX Manager 调用 Component Try 接口时需要传递)
  4. TX Manager 需要为这笔新开启的分布 式事务分配一个全局唯一的事务主键 Transaction ID
  5. TX Manager 将这笔分布式事务的明细记录添加到事务日志表中
  6. TX Manager 分别调用订单、账户、库存组件的 Try 接口,试探各个子模块的响应状况,比并尝试锁定对应的资源
  7. TX Manager 收集每个 TCC Component Try 接口的响应结果,根据结果决定下一轮的动作是 Confirm 还是 Cancel
  8. 倘若三笔 Try 请求中,有任意一笔未请求成功:
    • TX Manager 给予 Application 事务执行失败的 Response
    • TX Manager 批量调用订单、账户、库存 Component 的 Cancel 接口,回滚释放对应的资源
    • 在三笔 Cancel 请求都响应成功后,TX Manager 在事务日志表中将这笔事务记录置为【失败】状态
  9. 倘若三笔 Try 请求均响应成功了:
    • TX Manager 给予 Application 事务执行成功的 ACK
    • TX Manager 批量调用订单、账户、库存 Component 的 Confirm 接口,使得对应的变更记录实际生效
      在三笔 Confirm 请求都响应成功后,TX Manager 将这笔事务日志置为【成功】状态

图解分布式事务实现原理(二)_第4张图片
在上述流程中,有一个很重要的环节需要补充说明,首先,TCC 本质上是一个两阶段提交(Two Phase Commitment Protocol,2PC)的实现方案,分为 Try 和 Confirm/Cancel 的两个阶段

  • Try 操作的容错率是比较高的,原因在于有人帮它兜底. Try 只是一个试探性的操作,不论成功或失败,后续可以通过第二轮的 Confirm 或 Cancel 操作对最终结果进行修正
  • Confirm/Cancel 操作是没有容错的,倘若在第二阶段出现问题,可能会导致 Component 中的状态数据被长时间”冻结“或者数据状态不一致的问题

针对于这个场景,TCC 架构中采用的解决方案是:在第二阶段中,TX Manager 轮询重试 + TCC Component 幂等去重. 通过这两套动作形成的组合拳,保证 Confirm/ Cancel 操作至少会被 TCC Component 执行一次.

首先,针对于 TX Manager 而言:

  • 需要启动一个定时轮询任务
  • 对于事务日志表中,所有未被更新为【成功/失败】对应终态的事务,需要摘出进行检查
  • 检查时查看其涉及的每个组件的 Try 接口的响应状态以及这笔事务的持续时长
  • 倘若事务应该被置为【失败】(存在某个 TCC Component Try 接口请求失败),但状态却并未更新,说明之前批量执行 Cancel 操作时可能发生了错误. 此时需要补偿性地批量调用事务所涉及的所有 Component 的 Cancel 操作,待所有 Cancel 操作都成功后,将事务置为【失败】状态
  • 倘若事务应该被置为【成功】(所有 TCC Component Try 接口均请求成功),但状态却并未更新,说明之前批量执行 Confirm 操作时可能发生了错误. 此时需要补偿性地批量调用事务所涉及的所有 Component 的 Confirm 操作,待所有 Confirm 操作都成功后,将事务置为【成功】状态
  • 倘若事务仍处于【进行中】状态(TCC Component Try 接口请求未出现失败,但并非所有 Component Try 接口都请求成功),则检查事务的创建时间,倘若其耗时过长,同样需要按照事务失败的方式进行处理


需要注意,在 TX Manager 轮询重试的流程中,针对下游 TCC Component 的 Confirm 和 Cancel 请求只能保证 at least once 的语义,换句话说,这部分请求是可能出现重复的.

因此,在下游 TCC Component 中,需要在接收到 Confirm/Cancel 请求时,执行幂等去重操作. 幂等去重操作需要有一个唯一键作为去重的标识,这个标识键就是 TX Manager 在开启事务时为其分配的全局唯一的 Transaction ID,它既要作为这项事务在事务日志表中的唯一键,同时在 TX Manager 每次向 TCC Component 发起请求时,都要携带上这笔 Transaction ID.

TX Manager 职责

首先针对于事务协调器 TX Manager,其核心要点包括:

  • 注册TCC Component接口:该接口用于向分布式事务框架注册TCC组件(Try、Confirm、Cancel组件),以便管理和执行事务。不同的应用程序可能有不同的TCC组件需求,因此需要提供接口来注册和管理这些组件。

  • 启动分布式事务接口:作为与应用程序交互的唯一入口,该接口用于启动分布式事务。应用程序通过调用此接口来触发分布式事务的执行,并根据事务的执行结果进行反馈。

  • 全局唯一的Transaction ID:每个分布式事务需要一个全局唯一的事务标识,通常称为Transaction ID,用于跟踪和管理事务。同时,需要一个事务日志表来记录每项分布式事务的执行进展明细,以便进行事务状态的管理和恢复。

  • 串联Try-Confirm/Cancel两阶段流程:TCC模式的核心是将事务分为Try、Confirm和Cancel三个阶段,需要实现这三个阶段的串联执行流程。根据Try的结果,推进执行Confirm或Cancel流程,以确保事务的一致性。

  • 持续运行轮询检查任务:为了确保分布式事务的最终一致性,需要持续运行轮询检查任务,监测每个处于中间态的分布式事务,并将其推进到终态(已确认或已取消)。这有助于处理各种异常情况和故障恢复。

图解分布式事务实现原理(二)_第5张图片

TCC Component 职责

对于 TCC Component 而言,其需要关心和处理的工作包括:

  • 暴露出 Try、Confirm、Cancel 三个入口,对应于 TCC 的语义
  • 针对数据记录,新增出一个对应于 Try 操作的中间状态枚举值
  • 针对于同一笔事务的重复请求,需要执行幂等性校验
  • 需要支持空回滚操作. 即针对于一笔新的 Transaction ID,在没收到 Try 的前提下,若提前收到了 Cancel 操作,也需要将这个信息记录下来,但不需要对真实的状态数据发生变更

下面针对最后一点提到的空回滚操作,进一步加以说明:

这个空回滚机制本质上是为了解决 TCC 流程中出现的悬挂问题,下面我们举个具体例子加以说明:

具体来说,如果TX Manager在向Component A发起Try请求时发生了超时,并且TX Manager已经批量执行了Cancel操作,然后之前由于网络问题而阻塞的Try请求到达了Component A,这可能导致请求到达的次序颠倒。在这种情况下,Component A需要确保只要接收到了对应的Cancel请求,之后到来的Try请求需要被忽略,以维护事务的一致性。

支持空回滚操作是TCC组件的一项关键功能。通过空回滚操作,即使Try请求到达的次序发生颠倒,Component A仍然可以正确处理事务。当Component A收到Cancel请求时,它可以标记相应的事务为已取消状态,并且在之后收到的Try请求到达时,可以忽略它们,因为事务已经被取消。

这种机制确保了TCC事务模式的鲁棒性,即使在不稳定的网络环境下,也能够维护事务的一致性。空回滚操作是一种有效的方式来处理请求到达顺序的问题,确保分布式事务的可靠性。

TCC 优劣势分析

最后我们针对 TCC 分布式事务实现方案的优劣势进行分析:

优势:

  • TCC 可以称得上是真正意义上的分布式事务:任意一个 Component 的 Try 操作发生问题,都能支持事务的整体回滚操作
  • TCC 流程中,分布式事务中数据的状态一致性能够趋近于 100%,这是因为第二阶段 Confirm/Cancel 的成功率是很高的,原因在于如下三个方面:
    • TX Manager 在此前刚和 Component 经历过一轮 Try 请求的交互并获得了成功的 ACK,因此短时间内,Component 出现网络问题或者自身节点状态问题的概率是比较小的
    • TX Manager 已经通过 Try 操作,让 Component 提前锁定了对应的资源,因此确保了资源是充分的,且由于执行了状态锁定,出现并发问题的概率也会比较小
    • TX Manager 中通过轮询重试机制,保证了在 Confirm 和 Cancel 操作执行失败时,也能够通过重试机制得到补偿

劣势:

  • TCC 分布式事务中,涉及的状态数据变更只能趋近于最终一致性,无法做到即时一致性
  • 事务的原子性只能做到趋近于 100%,而无法做到真正意义上的 100%,原因就在于第二阶段的 Confirm 和 Cancel 仍然存在极小概率发生失败,即便通过重试机制也无法挽救. 这部分小概率事件,就需要通过人为介入进行兜底处理
  • TCC 架构的实现成本是很高的,需要所有子模块改造成 TCC 组件的格式,且整个事务的处理流程是相对繁重且复杂的. 因此在针对数据一致性要求不那么高的场景中,通常不会使用到这套架构.

事实上,上面提到的第二点劣势也并非是 TCC 方案的缺陷,而是所有分布式事务都存在的问题,由于网络请求以及第三方系统的不稳定性,分布式事务永远无法达到 100% 的原子性.


总结

Transaction Message:能够支持狭义的分布式事务. 基于消息队列组件中半事务消息以及轮询检查机制,保证了本地事务和消息生产两个动作的原子性,但不具备事务的逆向回滚能力
TCC Transaction:能够支持广义的分布式事务. 架构中每个模块需要改造成实现 Try/Confirm/Cancel 能力的 TCC 组件,通过事务协调器进行全局 Try——Confirm/Cancel 两阶段流程的串联,保证数据的最终一致性趋近于 100%

你可能感兴趣的:(go语言,分布式,分布式,golang,后端,面试)