Ch01 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 事务核心特性

# 1.2 分布式事务场景问题
下面我们通过一个常见的场景问题引出有关于分布式事务的话题.
假设我们在维护一个电商后台系统,每当在处理一笔来自用户创建订单的请求时,需要执行两步操作:
- 从账户系统中,扣减用户的账户余额
- 从库存系统中,扣减商品的剩余库存
从业务流程上来说,这个流程需要保证具备事务的原子性,即两个操作需要能够一气呵成地完成执行,要么同时成功,要么同时失败,不能够出现数据状态不一致的问题,比如发生从用户账户扣除了金额但商品库存却扣减失败的问题.
然而从技术流程上来讲,两个步骤是相对独立的两个操作,底层涉及到的存储介质也是相互独立的,因此无法基于本地事务的实现方式.
因此,这本质上就是我们今天所要谈及的分布式事务问题.
分布式事务的实现难度是很高的,但是不用慌,办法总比困难多,在业界针对于分布式事务早已提出一套被广泛认可应用的解决方案,这部分内容将在本文第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 的语义.
依赖于 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:先执行本地事务,后执行消息投递

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

组合 II 的优势:不会出现本地事务执行成功而消息投递失败的问题. 因为在消息投递失败时,可以不开启本地事务的执行操作.
组合 II 的劣势:可能出现消息投递成功而本地事务执行失败问题. 比如消息投递成功后,本地事务始终无法成功执行,而消息一经发出,就已经覆水难收了.
捋完上述两种流程中存在的问题后,一种比较容易想到的实现思路是:基于本地事务包裹消息投递操作的实现方式,对应执行步骤如下:
- 首先 begin transaction,开启本地事务
- 在事务中,执行本地状态数据的更新
- 完成数据更新后,不立即 commit transaction
- 执行消息投递操作
- 倘若消息投递成功,则 commit transaction
- 倘若消息投递失败,则 rollback transaction
这个流程乍一看没啥毛病,重复利用了本地事务回滚的能力,解决了本地修改操作成功、消息投递失败后本地数据修正成本高的问题.
然而,这仅仅是表现. 上述流程实际上是经不住推敲的,其中存在三个致命问题:
- 在和数据库交互的本地事务中,夹杂了和第三方组件的 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 侧本地事务的执行状态,从而引导半事务消息走向终态

在 TX Msg 的实现流程中,能够保证 2.3 小节中谈及的各种 badcase 都能被很好地消化:
- 倘若本地事务执行失败,则 producer 会向 RocketMQ 发出删除半事务消息的回滚指令,因此保证消息不会被发出
- 倘若本地事务执行成功, 则 producer 会向 RocketMQ 发出事务成功的确认指令,因此消息能够被正常发出
- 倘若 producer 端在发出第二轮的确认或回滚指令前发生意外状况,导致第二轮结果指令缺失. 则 RocketMQ 会基于自身的轮询机制主动询问本地事务的执行状况,最终帮助半事务消息推进进度.
RocketMQ 中半事务消息轮询流程示意如下:
最后,我们再回过头把 RocketMQ TX Msg 的使用交互流程总结梳理如下:

# 2.5 事务消息局限性
现在我们就来总结梳理一下,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 操作的基础上,真正提交这次修改操作还是回滚这次变更操作

# 3.2 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 接口
# 3.3 TCC 案例分析
现在假设我们需要维护一个电商后台系统,需要处理来自用户的支付请求. 每当有一笔支付请求到达,我们需要执行下述三步操作,并要求其前后状态保持一致性:
- 在订单模块中,创建出这笔订单流水记录
- 在账户模块中,对用户的账户进行相应金额的扣减
- 在库存模块中,对商品的库存数量进行扣减
上面这三步操作分别需要对接订单、账户、库存三个不同的子模块,底层的状态数据是基于不同的数据库和存储组件实现的,并且我们这套后台系统是基于当前流行的微服务架构实现的,这三子个模块本身对应的就是三个相互独立的微服务,因此如何实现在一笔支付请求处理流程中,使得这三笔操作对应的状态数据始终保持高度一致性,就成了一个非常具有技术挑战性的问题.
首先,我们基于 TCC 的设计理念,将订单模块、账户模块、库存模块分别改造成三个 TCC Component,每个 Component 对应需要暴露出 Try、Confirm、Cancel 三个 API,对应于冻结资源、确认更新资源、回滚解冻资源三个行为.
同时,为了能够简化后续 TX Manager 和 Application 之间的交互协议,每个 TCC Component 会以插件的形式提前注册到 TX Manager 维护的组件市场 Component Market 中,并提前声明好一个全局唯一键与之进行映射关联.
由于每个 TCC Component 需要支持 Try 接口的锁定操作,因此其中维护的数据需要在明细记录中拆出一个用于标识 “冻结” 状态的标签,或者在状态机中拆出一个 “冻结” 状态.
最终在第二阶段的 Confirm 或者 Cancel 请求到达时,再把 ”冻结“ 状态调整为 ”成功“ 或者 ”失败“ 的终态.
下面描述一下,基于 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 将这笔事务日志置为【成功】状态

在上述流程中,有一个很重要的环节需要补充说明:
首先,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 职责

对于 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 需要支持空回滚操作的原因所在.
# 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表」**,其中保存了每一次本地事务的回滚数据。
具体流程:
首先TM 向 TC 申请**「开启一个全局事务」,全局事务「创建」成功并生成一个「全局唯一的 XID」**。
XID 在微服务调用链路的上下文中传播。
RM 开始执行这个分支事务,RM首先解析这条SQL语句,「生成对应的UNDO_LOG记录」。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。
RM在同一个本地事务中**「执行业务SQL和UNDO_LOG数据的插入」。在提交这个本地事务前,RM会向TC「申请关于这条记录的全局锁」**。
如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。
RM在事务提交前,「申请到了相关记录的全局锁」,然后直接提交本地事务,并向TC**「汇报本地事务执行成功」**。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。
TC根据所有的分支事务执行结果,向RM**「下发提交或回滚」**命令。
RM如果**「收到TC的提交命令」,首先「立即释放」相关记录的全局「锁」**,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
RM如果「收到TC的回滚命令」,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,
- 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。
# XA模型

- 2PC要等到第二阶段才能释放锁
# AT模型

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

# 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