Ch01 Distributed Transaction

2/1/2023 Distributed Transaction

# 分布式理论

# CAP定理

在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」

一致性(C): 在分布式系统中的所有数据备份 (opens new window)「在同一时刻是否拥有同样的值」。(等同于所有节点访问同一份最新的数据副本)

可用性(A): 在集群中一部分节点**「故障」后,集群整体「是否还能响应」**客户端的读写请求。(对数据更新具备高可用性)

分区容错性(P): 即使出现**「单个组件无法可用,操作依然可以完成」**。

具体地讲在分布式系统中,在任何数据库设计中,一个Web应用**「至多只能同时支持上面的两个属性」**。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。

# BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?

前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • 「Basically Available(基本可用)」
  • 「Soft state(软状态)」
  • 「Eventually consistent(最终一致性)」

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

# 2PC

熟悉mysql的同学对两阶段提交应该颇为熟悉,mysql的事务就是通过**「日志系统」**来完成两阶段提交的。

  • 对于分布式事务一致性的研究成果包括著名的两阶段提交算法(Two-phase Commit,2PC)和三阶段提交算法(Three-phase Commit,3PC)。

    两阶段提交算法最早由 Jim Gray 于 1979 年在论文《Notes on Database Operating Systems》中提出。其基本思想十分简单,既然在分布式场景下,直接提交事务可能出现各种故障和冲突,那么可将其分解为预提交和正式提交两个阶段,规避风险。

    • 预提交(PreCommit):协调者(Coordinator)发起执行某个事务的执行并等待结果。各参与者(Participant)执行事务但不提交,反馈是否能完成,并阻塞等待协调者指令;
    • 正式提交(DoCommit):协调者如果得到所有参与者的成功答复,则发出正式提交指令,否则发出状态回滚指令。

    两阶段提交算法因为其简单容易实现的优点,在关系型数据库等系统中被广泛应用。当然,其缺点也很明显。

    • 第一阶段时,各参与者同步阻塞等待时无法处理请求,会导致系统性较差;
    • 存在协调者单点故障问题,最坏情况下协调者总是在第二阶段故障,无法完成提交;
    • 可能产生数据不一致的情况。例如第二个阶段时,协调者将正式提交请求发给部分参与者后发生故障。

# 3PC

三阶段提交(3PC)

三阶段提交又称3PC,相对于2PC来说增加了CanCommit阶段和超时机制。如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?

  • 第一阶段:

    「CanCommit阶段」

    这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。

    • 如果都返回yes,则进入第二阶段
    • 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
  • 第二阶段:**「PreCommit阶段」**此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。

  • 第三阶段:**「DoCommit阶段」**在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

# 分布式事务

# 1 分布式事务场景

# 1.1 事务核心特性

image-20231103110833498

# 1.2 分布式事务场景问题

下面我们通过一个常见的场景问题引出有关于分布式事务的话题.

假设我们在维护一个电商后台系统,每当在处理一笔来自用户创建订单的请求时,需要执行两步操作:

  • 从账户系统中,扣减用户的账户余额
  • 从库存系统中,扣减商品的剩余库存

从业务流程上来说,这个流程需要保证具备事务的原子性,即两个操作需要能够一气呵成地完成执行,要么同时成功,要么同时失败,不能够出现数据状态不一致的问题,比如发生从用户账户扣除了金额但商品库存却扣减失败的问题.

然而从技术流程上来讲,两个步骤是相对独立的两个操作,底层涉及到的存储介质也是相互独立的,因此无法基于本地事务的实现方式.

因此,这本质上就是我们今天所要谈及的分布式事务问题.

image-20231103112111055

分布式事务的实现难度是很高的,但是不用慌,办法总比困难多,在业界针对于分布式事务早已提出一套被广泛认可应用的解决方案,这部分内容将在本文第2、3章内容中详细展开介绍. 在这里,我们需要明确所谓分布式事务的实现中,其中所谓的数据状态一致性是需要做出妥协的:

  • 在分布式事务中,我们谈到的“数据状态一致性”,指的是数据的最终一致性,而非数据的即时一致性,因为即时一致性通常是不切实际的
  • 没有分布式事务能够保证数据状态具备百分之百的一致性,根本原因就在于网络环境和第三方系统的不稳定性

# 2 事务消息方案

首先,一类偏狭义的分布式事务解决方案是基于消息队列 MessageQueue(后续简称 MQ)实现的事务消息 Transaction Message.

# 2.1 RocketMQ 简介

RocketMQ 是阿里基于 java 实现并托管于 apache 基金会的顶级开源消息队列组件,其中事务消息 TX Msg 也是 RocketMQ 现有的一项能力. 本章将主要基于 RocketMQ 针对事务消息的实现思路展开介绍.

# 2.2 基于 MQ 实现分布式事务

我们知道在 MQ 组件中,通常能够为我们保证的一项能力是:投递到 MQ 中的消息能至少被下游消费者 consumer 消费到一次,即所谓的 at least once 语义.

基于此,MQ 组件能够保证消息不会在消费环节丢失,但是无法解决消息的重复性问题. 因此,倘若我们需要追求精确消费一次的目标,则下游的 consumer 还需要基于消息的唯一键执行幂等去重操作,在 at least once 的基础上过滤掉重复消息,最终达到 exactly once 的语义.

image-20231103112704469

依赖于 MQ 中 at least once 的性质,我们简单认为,只要把一条消息成功投递到 MQ 组件中,它就一定被下游 consumer 端消费端,至少不会发生消息丢失的问题.

倘若我们需要执行一个分布式事务,事务流程中包含需要在服务 A 中执行的动作 I 以及需要在服务 B 中执行的动作 II,此时我们可以基于如下思路串联流程:

  • 以服务 A 作为 MQ 生产方 producer,服务 B 作为 MQ 消费方 consumer
  • 服务 A 首先在执行动作 I,执行成功后往 MQ 中投递消息,驱动服务 B 执行动作 II
  • 服务 B 消费到消息后,完成动作 II 的执行

对上述流程进行总结,其具备如下优势

  • 服务 A 和服务 B 通过 MQ 组件实现异步解耦,从而提高系统处理整个事务流程的吞吐量
  • 当服务 A 执行 动作 I 失败后,可以选择不投递消息从而熔断流程,因此保证不会出现动作 II 执行成功,而动作 I 执行失败的不一致问题
  • 基于 MQ at least once 的语义,服务 A 只要成功消息的投递,就可以相信服务 B 一定能消费到该消息,至少服务 B 能感知到动作 II 需要执行的这一项情报
  • 依赖于 MQ 消费侧的 ack 机制,可以实现服务 B 有限轮次的重试能力. 即当服务 B 执行动作 II 失败后,可以给予 MQ bad ack,从而通过消息重发的机制实现动作 II 的重试,提高动作 II 的执行成功率

与之相对的,上述流程也具备如下几项局限性

  • 问题 1:服务 B 消费到消息执行动作 II 可能发生失败,即便依赖于 MQ 重试也无法保证动作一定能执行成功,此时缺乏令服务 A 回滚动作 I 的机制. 因此很可能出现动作 I 执行成功,而动作 II 执行失败的不一致问题
  • 问题 2:在这个流程中,服务 A 需要执行的操作有两步:(1)执行动作 I;(2)投递消息. 这两个步骤本质上也无法保证原子性,即可能出现服务 A 执行动作 I 成功,而投递消息失败的问题.

在本章谈及的事务消息实现方案中:

  • 针对问题 1 是无能为力的,因为这个问题本身就脱离于事务消息的领域范畴之外,需要放到第 3 章中通过另一类分布式事务的实现方案加以解决.
  • 而针对于问题 2 的解决思路,则正是本章中所需要重点探讨的话题.

# 2.3 本地事务+消息投递

2.2 小节中,聊到的服务 A 所要执行的操作分为两步:本地事务+消息投递. 这里我们需要如何保证这两个步骤的执行能够步调统一呢,下面不妨一起来推演一下我们的流程设计思路:

首先,这两个步骤在流程中一定会存在一个执行的先后顺序,我们首先来思考看看不同的组织顺序可能会分别衍生出怎样的问题:

  • 组合 I:先执行本地事务,后执行消息投递image-20231103113434738

    • 组合 I 的优势不会出现消息投递成功而本地事务执行失败的情况. 这是因为在本地事务执行失败时,可以主动熔断消息投递的动作.
    • 组合 I 的劣势可能出现本地事务执行成功而消息投递失败的问题. 比如本地事务成功后,想要尝试执行消息投递操作时一直出现失败,最终消息无法发出. 此时由于本地事务已经提交,要执行回滚操作会存在着很高的成本.
  • 组合 II:先执行消息投递,后执行本地事务image-20231103113750004

    • 组合 II 的优势:不会出现本地事务执行成功而消息投递失败的问题. 因为在消息投递失败时,可以不开启本地事务的执行操作.

    • 组合 II 的劣势:可能出现消息投递成功而本地事务执行失败问题. 比如消息投递成功后,本地事务始终无法成功执行,而消息一经发出,就已经覆水难收了.

捋完上述两种流程中存在的问题后,一种比较容易想到的实现思路是:基于本地事务包裹消息投递操作的实现方式,对应执行步骤如下:

  • 首先 begin transaction,开启本地事务
  • 在事务中,执行本地状态数据的更新
  • 完成数据更新后,不立即 commit transaction
  • 执行消息投递操作
  • 倘若消息投递成功,则 commit transaction
  • 倘若消息投递失败,则 rollback transaction
image-20231103113906997

这个流程乍一看没啥毛病,重复利用了本地事务回滚的能力,解决了本地修改操作成功、消息投递失败后本地数据修正成本高的问题.

然而,这仅仅是表现. 上述流程实际上是经不住推敲的,其中存在三个致命问题:

  • 在和数据库交互的本地事务中,夹杂了和第三方组件的 IO 操作,可能存在引发长事务的风险
  • 执行消息投递时,可能因为超时或其他意外原因,导致出现消息在事实上已投递成功,但 producer 获得的投递响应发生异常的问题,这样就会导致本地事务被误回滚的问题
  • 在执行事务提交操作时,可能发生失败. 此时事务内的数据库修改操作自然能够回滚,然而 MQ 消息一经发出,就已经无法回收了.

# 2.4 事务消息原理

下面,我们就正式开始介绍解决这个问题的正解——事务消息 Transaction Message.

我们以 RocketMQ 中 TX Msg 的实现方案为例展开介绍. 首先抛出结论,TX Msg 能保证我们做到在本地事务执行成功的情况下,后置的投递消息操作能以接近百分之百的概率被发出. 其实现的核心流程为:

  • 生产方 producer 首先向 RocketMQ 生产一条半事务消息,此消息处于中间态,会暂存于 RocketMQ 不会被立即发出
  • producer 执行本地事务
  • 如果本地事务执行成功,producer 直接提交本地事务,并且向 RocketMQ 发出一条确认消息
  • 如果本地事务执行失败,producer 向 RocketMQ 发出一条回滚指令
  • 倘若 RocketMQ 接收到确认消息,则会执行消息的发送操作,供下游消费者 consumer 消费
  • 倘若 RocketMQ 接收到回滚指令,则会删除对应的半事务消息,不会执行实际的消息发送操作
  • 此外,在 RocketMQ 侧,针对半事务消息会有一个轮询任务,倘若半事务消息一直未收到来自 producer 侧的二次确认,则 RocketMQ 会持续主动询问 producer 侧本地事务的执行状态,从而引导半事务消息走向终态

image-20231103114634741

在 TX Msg 的实现流程中,能够保证 2.3 小节中谈及的各种 badcase 都能被很好地消化:

  • 倘若本地事务执行失败,则 producer 会向 RocketMQ 发出删除半事务消息的回滚指令,因此保证消息不会被发出
  • 倘若本地事务执行成功, 则 producer 会向 RocketMQ 发出事务成功的确认指令,因此消息能够被正常发出
  • 倘若 producer 端在发出第二轮的确认或回滚指令前发生意外状况,导致第二轮结果指令缺失. 则 RocketMQ 会基于自身的轮询机制主动询问本地事务的执行状况,最终帮助半事务消息推进进度.

RocketMQ 中半事务消息轮询流程示意如下:image-20231103114954020

最后,我们再回过头把 RocketMQ TX Msg 的使用交互流程总结梳理如下:

image-20231103115208615

# 2.5 事务消息局限性

image-20231103115319450

现在我们就来总结梳理一下,TX Msg 中存在的几项局限性

  • **流程高度抽象:**TX Msg 把流程抽象成本地事务+投递消息两个步骤. 然而在实际业务场景中,分布式事务内包含的步骤数量可能很多,因此就需要把更多的内容更重的内容糅合在所谓的“本地事务”环节中,上游 producer 侧可能会存在比较大的压力
  • **不具备逆向回滚能力:**倘若接收消息的下游 consumer 侧执行操作失败,此时至多只能依赖于 MQ 的重发机制通过重试动作的方式提高执行成功率,但是无法从根本上解决下游 consumer 操作失败后回滚上游 producer 的问题. 这一点正是 TX Msg 中存在的最大的局限性.

关于上面第二点,我们再展开谈几句. 我们知道,并非所有动作都能通过简单的重试机制加以解决.

打个比方,倘若下游是一个库存管理系统,而对应商品的库存在事实上已经被扣减为 0,此时无论重试多少次请求都是徒然之举,这就是一个客观意义上的失败动作.

而遵循正常的事务流程,后置操作失败时,我们应该连带前置操作一起执行回滚,然而这部分能力在 TX Msg 的主流程中并没有予以体现.

要实现这种事务的逆向回滚能力,就必然需要构筑打通一条由下游逆流而上回调上游的通道,这一点并不属于 TX Msg 探讨的范畴.

# 3 TCC 实现方案

# 3.1 TCC 概念简述

TCC,全称 Try-Confirm-Cancel,指的是将一笔状态数据的修改操作拆分成两个阶段:

  • 第一个阶段是 Try,指的是先对资源进行锁定,资源处于中间态但不处于最终态

  • 第二个阶段分为 Confirm 和 Cancel,指的是在 Try 操作的基础上,真正提交这次修改操作还是回滚这次变更操作

    image-20231103115906710

# 3.2 TCC 宏观架构

image-20231103120002033

在 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 接口

# 3.3 TCC 案例分析

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

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

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

image-20231103120635843

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

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

image-20231103120818656

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

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

image-20231103120930594

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

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

image-20231103122744136

在上述流程中,有一个很重要的环节需要补充说明:

首先,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.

# 3.4 TX Manager 职责

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

  • 暴露出注册 TCC Component 的接口,进行 Component 的注册和管理
  • 暴露出启动分布式事务的接口,作为和 Application 交互的唯一入口,并基于 Application 事务执行结果的反馈
  • 为每个事务维护全局唯一的 Transaction ID,基于事务日志表记录每项分布式事务的进展明细
  • 串联 Try——Confirm/Cancel 的两阶段流程,根据 Try 的结果,推进执行 Confirm 或 Cancel 流程
  • 持续运行轮询检查任务,推进每个处于中间态的分布式事务流转到终态

# 3.5 TCC Component 职责

image-20231103122912438

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

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

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

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

  • TX Manager 在向 Component A 发起 Try 请求时,由于出现网络拥堵,导致请求超时
  • TX Manager 发现存在 Try 请求超时,将其判定为失败,因此批量执行 Component 的 Cancel 操作
  • Component A 率先收到了后发先至的 Cancel 请求
  • 过了一会儿,之前阻塞在网络链路中的 Try 请求也到达了 Component A

从执行逻辑上,Try 应该先于 Cancel 到达和处理,然而在事实上,由于网络环境的不稳定性,请求到达的先后次序可能颠倒. 在这个场景中,Component A 需要保证的是,针对于同一笔事务,只要接受过对应的 Cancel 请求,之后到来的 Try 请求需要被忽略. 这就是 TCC Component 需要支持空回滚操作的原因所在.

image-20231103123116491

# 3.6 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% 的原子性.

# 4 Seata框架

Saga事务模型又叫做长时间运行的事务

其核心思想是**「将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作」**。

Seata框架中一个分布式事务包含3种角色:

  • 「Transaction Coordinator (TC)」:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • 「Transaction Manager (TM)」:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • 「Resource Manager (RM)」:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

seata框架**「为每一个RM维护了一张UNDO_LOG表」**,其中保存了每一次本地事务的回滚数据。

具体流程

  1. 首先TM 向 TC 申请**「开启一个全局事务」,全局事务「创建」成功并生成一个「全局唯一的 XID」**。

  2. XID 在微服务调用链路的上下文中传播。

  3. RM 开始执行这个分支事务,RM首先解析这条SQL语句,「生成对应的UNDO_LOG记录」。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。

  4. RM在同一个本地事务中**「执行业务SQL和UNDO_LOG数据的插入」。在提交这个本地事务前,RM会向TC「申请关于这条记录的全局锁」**。

  5. 如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。

  6. RM在事务提交前,「申请到了相关记录的全局锁」,然后直接提交本地事务,并向TC**「汇报本地事务执行成功」**。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。

  7. TC根据所有的分支事务执行结果,向RM**「下发提交或回滚」**命令。

    • RM如果**「收到TC的提交命令」,首先「立即释放」相关记录的全局「锁」**,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。

    • RM如果「收到TC的回滚命令」,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,

      • 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
      • 如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。

# XA模型

image-20231029152933057

  • 2PC要等到第二阶段才能释放锁

# AT模型

image-20231029153054336

  • 直接提交,利用undo log来进行回滚
  • 有脏读问题:全局事务锁(行锁)表(每次提交事务都往里面增加一行),再最终提交之前不会删除,但是其他系统也要用seata

image-20231029152748388

# SAGA

https://cloud.tencent.com/developer/article/1839642

# 总结

事务消息和 TCC 事务两种分布式事务实现方案的技术原理:

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

ref:https://zhuanlan.zhihu.com/p/648556608

#

Last Updated: 11/19/2024, 1:54:38 PM