万字长文漫谈分布式事务实现原理

万字长文漫谈分布式事务实现原理

原创 小徐先生1212 [小徐先生的编程世界](javascript:void(0)😉 2023-08-11 20:12 发表于北京

0 前言

近期在工作当中和导师中哥一起参与了一个道具服务的设计与开发,由于这个服务的主链路涉及到与分布式事务有关的交互流程,由此引发我对这个话题产生了一些思考. 接下来两周,我会和大家一起分享探讨有关于分布式事务的话题,本系列分为上下两篇:

  • • 上篇:万字长文漫谈分布式事务实现原理
  • • 下篇:从零到一搭建 TCC 分布式事务框架

今天分享的是其中的上篇,下面正式开始.

1 分布式事务场景

1.1 事务核心特性

在聊分布式事务之前,我们先理清楚有关于 “事务” 的定义.

事务 Transaction,是一段特殊的执行程序,其需要具备如下四项核心性质:

图片

上面四项核心要素被称为事务的 acid 四大特性. 当事务的影响范围局限在一个关系型数据库范围内时,很多时候上述四项性质是能够水到渠成地得到实现的,但是倘若事务涉及修改的对象是跨数据库甚至跨服务跨存储组件时,这个问题就开始变得复杂且有趣了,也就正式迈入我们今天所重点探讨的“分布式事务”领域的问题.

1.2 分布式事务场景问题

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

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

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

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

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

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

图片

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

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

2 事务消息方案

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

2.1 RocketMQ 简介

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

RocketMQ github 地址:https://github.com/apache/rocketmq

图片

有关于 RocketMQ 中 TX Msg 的有关介绍,可以参见官方文档:https://rocketmq.apache.org/docs/4.x/producer/06message5/

在本章中,我亦会根据自己的个人理解,对这个流程加以描述和润色.

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 的劣势:可能出现本地事务执行成功而消息投递失败的问题. 比如本地事务成功后,想要尝试执行消息投递操作时一直出现失败,最终消息无法发出. 此时由于本地事务已经提交,要执行回滚操作会存在着很高的成本.

  • • 组合 II:先执行消息投递,后执行本地事务

图片

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

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

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

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

图片

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

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

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

2.4 事务消息原理

2.3 小节聊完后,我猜测大家心头多少产生一点点窒息感,同时也对我们今天所探讨的分布式事务这个话题产生了更充分的敬畏之心.

下面,我们就正式开始介绍解决这个问题的正解——事务消息 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 中存在的几项局限性:

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

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

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

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

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

我相信,能跟着我的思路一起走到这里的同学,一定都是对技术抱着主动求知、精益求精的积极态度,针对于 TX Msg 中存在的问题,心中一定是迫切地希望能寻求到一个合适的解决方案. 那么请大家不用着急,有关于这部分内容,我们就在第 3 章内容中见分晓.

3 TCC 实现方案

3.1 TCC 概念简述

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

  • • 第一个阶段是 Try,指的是先对资源进行锁定,资源处于中间态但不处于最终态
  • • 第二个阶段分为 Confirm 和 Cancel,指的是在 Try 操作的基础上,真正提交这次修改操作还是回滚这次变更操作

图片

更多有关于 TCC 的概念介绍,可以参见这篇文档:https://www.bytesoft.org/tcc-intro/ . 个人觉得内容还是比较优质.

3.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 接口

3.3 TCC 案例分析

前面我们大致捋了一遍 TCC 的整体架构,但干聊概念还是过于抽象,理解认知上不够形象和立体,下面我们引入一个具体的分布式事务场景问题,并通过 TCC 架构加以实现,帮助大家进一步提高对 TCC 分布式事务方案的感性认识.

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

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

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

图片

下面我们就撸起袖子开整.

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

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

图片

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

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

![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

下面描述一下,基于 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 将这笔事务日志置为【成功】状态

![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

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

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

![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

3.4 TX Manager 职责

通过上面这个实际案例的流程剖析后,相信大家已经对这套 TCC 架构有了一定的理解. 接下来我们再从 TX Manager 和 TCC Component 职责领域划分的视角出发,进行一轮梳理.

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

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

3.5 TCC Component 职责

![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

对于 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 需要支持空回滚操作的原因所在.

![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

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

4 总结

本期和大家一起分享了事务消息和 TCC 事务两种分布式事务实现方案的技术原理:

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

我个人一直秉持的观点是——理论需要得到实践的检验. 今天咱们看似聊了很多,但都是偏抽象和空洞的原理性内容. 这里咱们提前做个预告,接下来我会基于今天聊到的这套 TCC 分布式事务的技术方案进行落地实践,基于 Golang 从零到一搭建出一个 TCC 分布式事务框架,敬请期待!

目前基于 Golang 实现的 TCC 框架已于 github 开源:https://github.com/xiaoxuxiansheng/gotcc 大家走过路过,留个 star,不胜感激!

关注后可发消息

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MySQL多数据源是指在一个应用程序中同时使用多个不同的MySQL数据库来存储和管理数据的技术。它可以帮助开发人员更灵活地处理各种数据库操作,提高程序的性能和可扩展性。下面是一个完整的MySQL多数据源教程。 一、设置数据库连接信息 1. 在应用程序的配置件中,创建多个数据库连接的配置项。例如,可以为每个数据源创建一个配置项,分别命名为db1、db2等。 2. 在配置项中,设置每个数据源的连接信息,包括数据库地址、用户名、密码等。 二、创建数据源管理器 1. 创建一个数据源管理器类,用于管理多个数据源。该类需要实现数据源的动态切换和获取。 2. 使用Java的线程安全的数据结构,如ConcurrentHashMap来存储数据源信息。将配置件中的数据库连接信息加载到数据结构中。 3. 实现方法来切换不同的数据源,通过传入数据源的名称来切换到对应的数据库。 三、实现数据源切换 1. 在应用程序中,根据业务需求选择需要使用的数据源。可以通过调用数据源管理器的方法来切换数据源。 2. 在DAO层的代码中,根据当前使用的数据源名称,选择对应的数据源进行数据库操作。 四、使用多数据源进行数据库操作 1. 在DAO层的代码中,区分不同的数据源,并将数据库操作的代码包装在对应的数据源中。 2. 在业务层的代码中,调用DAO层的方法来进行数据库操作。不同的数据源会自动切换。 五、处理事务 1. 如果需要在一个事务中操作多个数据源,可以使用分布式事务的方式来处理。 2. 可以使用开源的分布式事务框架,如Atomikos、Bitronix等来实现多数据源的事务管理。 六、监控和维护 1. 使用监控工具来监控多个数据源的使用情况,包括连接数、查询次数等。 2. 定期对数据库进行维护,包括索引优化、数据清理等工作,以保证数据库的性能和稳定性。 通过以上步骤,我们可以实现MySQL多数据源的配置和使用。使用多数据源可以更好地管理和处理不同的数据库操作,在提高程序性能和可扩展性的同时,也提供了更灵活的数据操作方式。同时,需要注意合理选择和配置数据源,以及监控和维护数据库,以保证系统的运行效率和数据的安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值