关于java:一万四千字分布式事务原理解析全部掌握你还怕面试被问

6次阅读

共计 12551 个字符,预计需要花费 32 分钟才能阅读完成。

前言

从 CPU 到内存、到磁盘、到操作系统、到网络,计算机系统处处存在不牢靠因素。工程师和科学家致力应用各种软硬件办法反抗这种不牢靠因素,保证数据和指令被正确地解决。在网络畛域有 TCP 牢靠传输协定、在存储畛域有 Raid5 和 Raid6 算法、在数据库畛域有基于 ARIES 算法实践实现的事务机制……

这篇文章先介绍单机数据库事务的 ACID 个性,而后指出分布式场景下操作多数据源面临的窘境,引出分布式系统中罕用的分布式事务解决方案,这些解决方案能够保障业务代码在操作多个数据源的时候,可能像操作单个数据源一样,具备 ACID 个性。文章在最初给出业界较为成熟的分布式事务框架——Seata 的 AT 模式全局事务的实现。

一、单数据源事务 & 多数据源事务

如果一个应用程序在一次业务流中通过连贯驱动和数据源接口只连贯并查问(这里的查问是狭义的,包含增删查改等)一个特定的数据库,该应用程序就能够利用数据库提供的事务机制(如果数据库反对事务的话)保障对库中记录所进行的操作的可靠性,这里的可靠性有四种语义:

  • 原子性,A
  • 一致性,C
  • 隔离性,I
  • 持久性,D

笔者在这里不再对这四种语义进行解释,理解单数据源事务及其 ACID 个性是读者浏览这篇文章的前提。单个数据库实现本身的事务个性是一个简单又奥妙的过程,例如 MySQL 的 InnoDB 引擎通过 Undo Log + Redo Log + ARIES 算法来实现。这是一个很巨大的话题,不在本文的形容范畴,读者有趣味的话可自行钻研。

单数据源事务也能够叫做单机事务,或者本地事务。

在分布式场景下,一个零碎由多个子系统形成,每个子系统有独立的数据源。多个子系统之间通过相互调用来组合出更简单的业务。在时下风行的微服务零碎架构中,每一个子系统被称作一个微服务,同样每个微服务都保护本人的数据库,以放弃独立性。

例如,一个电商零碎可能由购物微服务、库存微服务、订单微服务等组成。购物微服务通过调用库存微服务和订单微服务来整合出购物业务。用户申请购物微服务商实现下单时,购物微服务一方面调用库存微服务扣减相应商品的库存数量,另一方面调用订单微服务插入订单记录(为了后文形容分布式事务解决方案的不便,这里给出的是一个最简略的电商零碎微服务划分和最简略的购物业务流程,后续的领取、物流等业务不在思考范畴内)。电商零碎模型如下图所示:

在用户购物的业务场景中,shopping-service 的业务波及两个数据库:库存库(repo_db)和订单库(repo_db),也就是 g 购物业务是调用多数据源来组合而成的。作为一个面向消费者的零碎,电商零碎要保障购物业务的高度可靠性,这里的可靠性同样有 ACID 四种语义。

然而一个数据库的本地事务机制仅仅对落到本人身上的查问操作(这里的查问是狭义的,包含增删改查等)起作用,无奈干预对其余数据库的查问操作。所以,数据库本身提供的本地事务机制无奈确保业务对多数据源全局操作的可靠性。

基于此,针对多数据源操作提出的分布式事务机制就呈现了。

分布式事务也能够叫做全局事务。

二、常见分布式事务解决方案

2.1 分布式事务模型

形容分布式事务,经常会应用以下几个名词:

  • 事务参与者:例如每个数据库就是一个事务参与者
  • 事务协调者:拜访多个数据源的服务程序,例如 shopping-service 就是事务协调者
  • 资源管理器(Resource Manager, RM):通常与事务参与者同义
  • 事务管理器(Transaction Manager, TM):通常与事务协调者同义

在分布式事务模型中,一个 TM 治理多个 RM,即一个服务程序拜访多个数据源;TM 是一个全局事务管理器,协调多方本地事务的进度,使其独特提交或回滚,最终达成一种全局的 ACID 个性。

2.2 二将军问题和幂等性

二将军问题是网络畛域的一个经典问题,用于表白计算机网络中互联协定设计的奥妙性和复杂性。这里给出一个二将军问题的简化版本:

一支白军被围困在一个山谷中,山谷的左右两侧是蓝军。困在山谷中的白军人数多于山谷两侧的任意一支蓝军,而少于两支蓝军的之和。若一支蓝军对白军独自发动防御,则必败无疑;但若两支蓝军同时发动防御,则可取胜。两只蓝军的总指挥位于山谷左侧,他心愿两支蓝军同时发动防御,这样就要把命令传到山谷右侧的蓝军,以告知发动防御的具体工夫。假如他们只能差遣士兵穿梭白军所在的山谷(惟一的通信信道)来传递音讯,那么在穿梭山谷时,士兵有可能被俘虏。

只有当送信士兵胜利往返后,总指挥能力确认这场和平的胜利(上方图)。当初问题来了,差遣进来送信的士兵没有回来,则左侧蓝军中的总指挥能不能决定按命令中约定的工夫发动防御?

答案是不确定,差遣进来送信的士兵没有回来,他可能遇到两种情况:

1)命令还没送达就被俘虏了(两头图),这时候右侧蓝军基本不晓得要何时防御;

2)命令送达,但返回途中被俘虏了(下方图),这时候右侧蓝军晓得要何时防御,但左侧蓝军不晓得右侧蓝军是否通晓防御工夫。

相似的问题在计算机网络中普遍存在,例如发送者给接受者发送一个 HTTP 申请,或者 MySQL 客户端向 MySQL 服务器发送一条插入语句,而后超时了没有失去响应。请问服务器是写入胜利了还是失败了?答案是不确定,有以下几种状况:

1)可能申请因为网络故障基本没有送到服务器,因而写入失败;

2)可能服务器收到了,也写入胜利了,然而向客户端发送响应前服务器宕机了;

3)可能服务器收到了,也写入胜利了,也向客户端发送了响应,然而因为网络故障未送到客户端。

无论哪种场景,在客户端看来都是一样的后果:它收回的申请没有失去响应。为了确保服务端胜利写入数据,客户端只能重发申请,直至接管到服务端的响应。

相似的问题问题被称为网络二将军问题。

网络二将军问题的存在使得音讯的发送者往往要反复发送音讯,直到收到接收者的确认才认为发送胜利,但这往往又会导致音讯的反复发送。例如电商零碎中订单模块调用领取模块扣款的时候,如果网络故障导致二将军问题呈现,扣款申请反复发送,产生的反复扣款后果显然是不能被承受的。因而要保障一次事务中的扣款申请无论被发送多少次,接管方有且只执行一次扣款动作,这种保障机制叫做接管方的幂等性。

2.3 两阶段提交(2PC)& 三阶段提交(3PC)计划

2PC 是一种实现分布式事务的简略模型,这两个阶段是:

1)筹备阶段:事务协调者向各个事务参与者发动询问申请:“我要执行全局事务了,这个事务波及到的资源散布在你们这些数据源中,别离是……,你们筹备好各自的资源(即各自执行本地事务到待提交阶段)”。各个参与者协调者回复 yes(示意已筹备好,容许提交全局事务)或 no(示意本参与者无奈拿到全局事务所需的本地资源,因为它被其余本地事务锁住了)或超时。

2)提交阶段:如果各个参与者回复的都是 yes,则协调者向所有参与者发动事务提交操作,而后所有参与者收到后各自执行本地事务提交操作并向协调者发送 ACK;如果任何一个参与者回复 no 或者超时,则协调者向所有参与者发动事务回滚操作,而后所有参与者收到后各自执行本地事务回滚操作并向协调者发送 ACK。

2PC 的流程如下图所示:

从上图能够看出,要实现 2PC,所有的参与者都要实现三个接口:

  • Prepare():TM 调用该接口询问各个本地事务是否就绪
  • Commit():TM 调用该接口要求各个本地事务提交
  • Rollback():TM 调用该接口要求各个本地事务回滚

能够将这三个接口简略地(但不谨严地)了解成 XA 协定。XA 协定是 X/Open 提出的分布式事务处理规范。MySQL、Oracle、DB2 这些支流数据库都实现了 XA 协定,因而都能被用于实现 2PC 事务模型。

2PC 扼要易懂,但存在如下的问题:

1)性能差,在筹备阶段,要期待所有的参与者返回,能力进入阶段二,在这期间,各个参与者下面的相干资源被排他地锁住,参与者下面用意应用这些资源的本地事务只能期待。因为存在这种同步阻塞问题,所以影响了各个参与者的本地事务并发度;

2)筹备阶段实现后,如果协调者宕机,所有的参与者都收不到提交或回滚指令,导致所有参与者“手足无措”;

3)在提交阶段,协调者向所有的参与者发送了提交指令,如果一个参与者未返回 ACK,那么协调者不晓得这个参与者外部产生了什么(因为网络二将军问题的存在,这个参与者可能基本没收到提交指令,始终处于期待接管提交指令的状态;也可能收到了,并胜利执行了本地提交,但返回的 ACK 因为网络故障未送到协调者上),也就无奈决定下一步是否进行整体参与者的回滚。

2PC 之后又呈现了 3PC,把两阶段过程变成了三阶段过程,别离是:询问阶段、筹备阶段、提交或回滚阶段,这里不再详述。3PC 利用超时机制解决了 2PC 的同步阻塞问题,防止资源被永恒锁定,进一步增强了整个事务过程的可靠性。然而 3PC 同样无奈应答相似的宕机问题,只不过呈现多数据源中数据不统一问题的概率更小。

2PC 除了性能和可靠性上存在问题,它的实用场景也很局限,它要求参与者实现了 XA 协定,例如应用实现了 XA 协定的数据库作为参与者能够实现 2PC 过程。然而在多个零碎服务利用 api 接口互相调用的时候,就不恪守 XA 协定了,这时候 2PC 就不实用了。所以 2PC 在分布式应用场景中很少应用。

所以前文提到的电商场景无奈应用 2PC,因为 shopping-service 通过 RPC 接口或者 Rest 接口调用 repo-service 和 order-service 间接拜访 repo_db 和 order_db。除非 shopping-service 间接配置 repo_db 和 order_db 作为本人的数据库。

2.4 TCC 计划

形容 TCC 计划应用的电商微服务模型如下图所示,在这个模型中,shopping-service 是事务协调者,repo-service 和 order-service 是事务参与者。

上文提到,2PC 要求参与者实现了 XA 协定,通常用来解决多个数据库之间的事务问题,比拟局限。在多个零碎服务利用 api 接口互相调用的时候,就不恪守 XA 协定了,这时候 2PC 就不实用了。古代企业多采纳分布式的微服务,因而更多的是要解决多个微服务之间的分布式事务问题。

TCC 就是一种解决多个微服务之间的分布式事务问题的计划。TCC 是 Try、Confirm、Cancel 三个词的缩写,其本质是一个利用层面上的 2PC,同样分为两个阶段:

1)阶段一:筹备阶段。协调者调用所有的每个微服务提供的 try 接口,将整个全局事务波及到的资源锁定住,若锁定胜利 try 接口向协调者返回 yes。

2)阶段二:提交阶段。若所有的服务的 try 接口在阶段一都返回 yes,则进入提交阶段,协调者调用所有服务的 confirm 接口,各个服务进行事务提交。如果有任何一个服务的 try 接口在阶段一返回 no 或者超时,则协调者调用所有服务的 cancel 接口。

TCC 的流程如下图所示:

这里有个关键问题,既然 TCC 是一种服务层面上的 2PC,它是如何解决 2PC 无奈应答宕机问题的缺点的呢?答案是一直重试。因为 try 操作锁住了全局事务波及的所有资源,保障了业务操作的所有前置条件失去满足,因而无论是 confirm 阶段失败还是 cancel 阶段失败都能通过一直重试直至 confirm 或 cancel 胜利(所谓胜利就是所有的服务都对 confirm 或者 cancel 返回了 ACK)。

这里还有个关键问题,在一直重试 confirm 和 cancel 的过程中(思考到网络二将军问题的存在)有可能反复进行了 confirm 或 cancel,因而还要再保障 confirm 和 cancel 操作具备幂等性,也就是整个全局事务中,每个参与者只进行一次 confirm 或者 cancel。实现 confirm 和 cancel 操作的幂等性,有很多解决方案,例如每个参与者能够保护一个去重表(能够利用数据库表实现也能够应用内存型 KV 组件实现),记录每个全局事务(以全局事务标记 XID 辨别)是否进行过 confirm 或 cancel 操作,若曾经进行过,则不再反复执行。

TCC 由支付宝团队提出,被广泛应用于金融零碎中。咱们用银行账户余额购买基金时,会留神到银行账户中用于购买基金的那局部余额首先会被解冻,由此咱们能够猜测,这个过程大略就是 TCC 的第一阶段。

2.5 事务状态表计划

另外有一种相似 TCC 的事务解决方案,借助事务状态表来实现。假如要在一个分布式事务中实现调用 repo-service 扣减库存、调用 order-service 生成订单两个过程。在这种计划中,协调者 shopping-service 保护一张如下的事务状态表:

初始状态为 1,每胜利调用一个服务则更新一次状态,最初所有的服务调用胜利,状态更新到 3。

有了这张表,就能够启动一个后台任务,扫描这张表中事务的状态,如果一个分布式事务始终(设置一个事务周期阈值)未到状态 3,阐明这条事务没有胜利执行,于是能够从新调用 repo-service 扣减库存、调用 order-service 生成订单。直至所有的调用胜利,事务状态到 3。

如果多次重试仍未使得状态到 3,能够将事务状态置为 error,通过人工染指进行干涉。

因为存在服务的调用重试,因而每个服务的接口要依据全局的分布式事务 ID 做幂等,原理同 2.4 节的幂等性实现。

2.7 基于消息中间件的最终一致性事务计划

无论是 2PC & 3PC 还是 TCC、事务状态表,根本都恪守 XA 协定的思维,即这些计划实质上都是事务协调者协调各个事务参与者的本地事务的进度,使所有本地事务独特提交或回滚,最终达成一种全局的 ACID 个性。在协调的过程中,协调者须要收集各个本地事务的以后状态,并依据这些状态收回下一阶段的操作指令。

然而这些全局事务计划因为操作繁琐、时间跨度大,或者在全局事务期间会排他地锁住相干资源,使得整个分布式系统的全局事务的并发度不会太高。这很难满足电商等高并发场景对事务吞吐量的要求,因而互联网服务提供商摸索出了很多与 XA 协定南辕北辙的分布式事务解决方案。其中利用消息中间件实现的最终一致性全局事务就是一个经典计划。

为了体现出这种计划的精华,我将应用如下的电商零碎微服务构造来进行形容:

在这个模型中,用户不再是申请整合后的 shopping-service 进行下单,而是间接申请 order-service 下单,order-service 一方面增加订单记录,另一方面会调用 repo-service 扣减库存。

这种基于消息中间件的最终一致性事务计划经常被误会成如下的实现形式:

这种实现形式的流程是:

1)order-service 负责向 MQ server 发送扣减库存音讯(repo_deduction_msg);repo-service 订阅 MQ server 中的扣减库存音讯,负责生产音讯。

2)用户下单后,order-service 先执行插入订单记录的查问语句,后将 repo_deduction_msg 发到消息中间件中,这两个过程放在一个本地事务中进行,一旦“执行插入订单记录的查问语句”失败,导致事务回滚,“将 repo_deduction_msg 发到消息中间件中”就不会产生;同样,一旦“将 repo_deduction_msg 发到消息中间件中”失败,抛出异样,也会导致“执行插入订单记录的查问语句”操作回滚,最终什么也没有产生。

3)repo-service 接管到 repo_deduction_msg 之后,先执行库存扣减查问语句,后向 MQ sever 反馈音讯生产实现 ACK,这两个过程放在一个本地事务中进行,一旦“执行库存扣减查问语句”失败,导致事务回滚,“向 MQ sever 反馈音讯生产实现 ACK”就不会产生,MQ server 在 Confirm 机制的驱动下会持续向 repo-service 推送该音讯,直到整个事务胜利提交;同样,一旦“向 MQ sever 反馈音讯生产实现 ACK”失败,抛出异样,也对导致“执行库存扣减查问语句”操作回滚,MQ server 在 Confirm 机制的驱动下会持续向 repo-service 推送该音讯,直到整个事务胜利提交。

这种做法看似很牢靠。但没有思考到网络二将军问题的存在,有如下的缺点:

1)存在网络的 2 将军问题,下面第 2)步中 order-service 发送 repo_deduction_msg 音讯失败,对于发送方 order-service 来说,可能是消息中间件没有收到音讯;也可能是中间件收到了音讯,但向发送方 order-service 响应的 ACK 因为网络故障没有被 order-service 收到。因而 order-service 贸然进行事务回滚,撤销“执行插入订单记录的查问语句”,是不对的,因为 repo-service 那边可能曾经接管到 repo_deduction_msg 并胜利进行了库存扣减,这样 order-service 和 repo-service 两方就产生了数据不统一问题。

2)repo-service 和 order-service 把网络调用(与 MQ server 通信)放在本地数据库事务里,可能会因为网络提早产生数据库长事务,影响数据库本地事务的并发度。

以上是被误会的实现形式,上面给出正确的实现形式,如下所示:

上图所示的计划,利用消息中间件如 rabbitMQ 来实现分布式下单及库存扣减过程的最终一致性。对这幅图做以下阐明:

1)order-service 中,

 在 t_order 表增加订单记录 &&

在 t_local_msg 增加对应的扣减库存音讯 

这两个过程要在一个事务中实现,保障过程的原子性。同样,repo-service 中,

 查看本次扣库存操作是否曾经执行过 &&

执行扣减库存如果本次扣减操作没有执行过 &&

写判重表 &&

向 MQ sever 反馈音讯生产实现 ACK

这四个过程也要在一个事务中实现,保障过程的原子性。

2)order-service 中有一个后台程序,源源不断地把音讯表中的音讯传送给消息中间件,胜利后则删除音讯表中对应的音讯。如果失败了,也会一直尝试重传。因为存在网络 2 将军问题,即当 order-service 发送给消息中间件的音讯网络超时时,这时候消息中间件可能收到了音讯但响应 ACK 失败,也可能没收到,order-service 会再次发送该音讯,直至消息中间件响应 ACK 胜利,这样可能产生音讯的反复发送,不过没关系,只有保障音讯不失落,不乱序就行,前面 repo-service 会做去重解决。

3)消息中间件向 repo-service 推送 repo_deduction_msg,repo-service 胜利解决实现后会向中间件响应 ACK,消息中间件收到这个 ACK 才认为 repo-service 胜利解决了这条音讯,否则会反复推送该音讯。然而有这样的情景:repo-service 胜利解决了音讯,向中间件发送的 ACK 在网络传输中因为网络故障失落了,导致中间件没有收到 ACK 从新推送了该音讯。这也要靠 repo-service 的音讯去重个性来防止音讯反复生产。

4)在 2)和 3)中提到了两种导致 repo-service 反复收到音讯的起因,一是生产者反复生产,二是中间件重传。为了实现业务的幂等性,repo-service 中保护了一张判重表,这张表中记录了被胜利解决的音讯的 id。repo-service 每次接管到新的音讯都先判断音讯是否被胜利解决过,若是的话不再反复解决。

通过这种设计,实现了音讯在发送方不失落,音讯在接管方不被反复生产,联结起来就是音讯不漏不重,严格实现了 order-service 和 repo-service 的两个数据库中数据的最终一致性。

基于消息中间件的最终一致性全局事务计划是互联网公司在高并发场景中摸索出的一种创新型利用模式,利用 MQ 实现微服务之间的异步调用、解耦合和流量削峰,反对全局事务的高并发,并保障分布式数据记录的最终一致性。

三、Seata in AT mode 的实现

第 2 章给出了实现实现分布式事务的集中常见的实践模型。本章给出业界开源分布式事务框架 Seata 的实现。

Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式。其中 AT 模式是 Seata 主推的事务模式,因而本章剖析 Seata in AT mode 的实现。应用 AT 有一个前提,那就是微服务应用的数据库必须是反对事务的关系型数据库。

3.1 Seata in AT mode 工作流程概述

Seata 的 AT 模式建设在关系型数据库的本地事务个性的根底之上,通过数据源代理类拦挡并解析数据库执行的 SQL,记录自定义的回滚日志,如需回滚,则重放这些自定义的回滚日志即可。AT 模式尽管是依据 XA 事务模型(2PC)演进而来的,然而 AT 突破了 XA 协定的阻塞性制约,在一致性和性能上获得了均衡。

AT 模式是基于 XA 事务模型演进而来的,它的整体机制也是一个改良版本的两阶段提交协定。AT 模式的两个根本阶段是:

1)第一阶段:首先获取本地锁,执行本地事务,业务数据操作和记录回滚日志在同一个本地事务中提交,最初开释本地锁;

2)第二阶段:如需全局提交,异步删除回滚日志即可,这个过程很快就能实现。如须要回滚,则通过第一阶段的回滚日志进行反向弥补。

本章形容 Seata in AT mode 的工作原理应用的电商微服务模型如下图所示:

在上图中,协调者 shopping-service 先调用参与者 repo-service 扣减库存,后调用参与者 order-service 生成订单。这个业务流应用 Seata in XA mode 后的全局事务流程如下图所示:

上图形容的全局事务执行流程为:

1)shopping-service 向 Seata 注册全局事务,并产生一个全局事务标识 XID

2)将 repo-service.repo_db、order-service.order_db 的本地事务执行到待提交阶段,事务内容蕴含对 repo-service.repo_db、order-service.order_db 进行的查问操作以及写每个库的 undo_log 记录

3)repo-service.repo_db、order-service.order_db 向 Seata 注册分支事务,并将其纳入该 XID 对应的全局事务范畴

4)提交 repo-service.repo_db、order-service.order_db 的本地事务

5)repo-service.repo_db、order-service.order_db 向 Seata 汇报分支事务的提交状态

6)Seata 汇总所有的 DB 的分支事务的提交状态,决定全局事务是该提交还是回滚

7)Seata 告诉 repo-service.repo_db、order-service.order_db 提交 / 回滚本地事务,若须要回滚,采取的是弥补式办法

其中 1)2)3)4)5)属于第一阶段,6)7)属于第二阶段。

3.1Seata in AT mode 工作流程详述

在下面的电商业务场景中,购物服务调用库存服务扣减库存,调用订单服务创立订单,显然这两个调用过程要放在一个事务外面。即:

start global_trx

 call 库存服务的扣减库存接口

 call 订单服务的创立订单接口

commit global_trx

在库存服务的数据库中,存在如下的库存表 t_repo:

在订单服务的数据库中,存在如下的订单表 t_order:

当初,id 为 40002 的用户要购买一只商品代码为 20002 的鼠标,整个分布式事务的内容为:

1)在库存服务的库存表中将记录

批改为

2)在订单服务的订单表中增加一条记录

以上操作,在 AT 模式的第一阶段的流程图如下:

从 AT 模式第一阶段的流程来看,分支的本地事务在第一阶段提交实现之后,就会开释掉本地事务锁定的本地记录。这是 AT 模式和 XA 最大的不同点,在 XA 事务的两阶段提交中,被锁定的记录直到第二阶段完结才会被开释。所以 AT 模式缩小了锁记录的工夫,从而进步了分布式事务的解决效率。AT 模式之所以可能实现第一阶段实现就开释被锁定的记录,是因为 Seata 在每个服务的数据库中保护了一张 undo_log 表,其中记录了对 t_order / t_repo 进行操作前后记录的镜像数据,即使第二阶段产生异样,只需回放每个服务的 undo_log 中的相应记录即可实现全局回滚。

undo_log 的表构造:

第一阶段完结之后,Seata 会接管到所有分支事务的提交状态,而后决定是提交全局事务还是回滚全局事务。

1)若所有分支事务本地提交均胜利,则 Seata 决定全局提交。Seata 将分支提交的音讯发送给各个分支事务,各个分支事务收到分支提交音讯后,会将音讯放入一个缓冲队列,而后间接向 Seata 返回提交胜利。之后,每个本地事务会缓缓解决分支提交音讯,解决的形式为:删除相应分支事务的 undo_log 记录。之所以只需删除分支事务的 undo_log 记录,而不须要再做其余提交操作,是因为提交操作曾经在第一阶段实现了(这也是 AT 和 XA 不同的中央)。这个过程如下图所示:

分支事务之所以可能间接返回胜利给 Seata,是因为真正要害的提交操作在第一阶段曾经实现了,革除 undo_log 日志只是收尾工作,即使革除失败了,也对整个分布式事务不产生本质影响。

2)若任一分支事务本地提交失败,则 Seata 决定全局回滚,将分支事务回滚音讯发送给各个分支事务,因为在第一阶段各个服务的数据库上记录了 undo_log 记录,分支事务回滚操作只需依据 undo_log 记录进行弥补即可。全局事务的回滚流程如下图所示:

这里对图中的 2、3 步做进一步的阐明:

1)因为上文给出了 undo_log 的表构造,所以能够通过 xid 和 branch_id 来找到以后分支事务的所有 undo_log 记录;

2)拿到以后分支事务的 undo_log 记录之后,首先要做数据校验,如果 afterImage 中的记录与以后的表记录不统一,阐明从第一阶段实现到此刻期间,有别的事务批改了这些记录,这会导致分支事务无奈回滚,向 Seata 反馈回滚失败;如果 afterImage 中的记录与以后的表记录统一,阐明从第一阶段实现到此刻期间,没有别的事务批改这些记录,分支事务可回滚,进而依据 beforeImage 和 afterImage 计算出弥补 SQL,执行弥补 SQL 进行回滚,而后删除相应 undo_log,向 Seata 反馈回滚胜利。

事务具备 ACID 个性,全局事务解决方案也在尽量实现这四个个性。以上对于 Seata in AT mode 的形容很显然体现出了 AT 的原子性、一致性和持久性。上面着重形容一下 AT 如何保障多个全局事务的隔离性的。

在 AT 中,当多个全局事务操作同一张表时,通过全局锁来保障事务的隔离性。上面形容一下全局锁在读隔离和写隔离两个场景中的作用原理:

1)写隔离(若有全局事务在改 / 写 / 删记录,另一个全局事务对同一记录进行的改 / 写 / 删要被隔离起来,即写写互斥):写隔离是为了在多个全局事务对同一张表的同一个字段进行更新操作时,防止一个全局事务在没有被提交胜利之前所波及的数据被其余全局事务批改。写隔离的基本原理是:在第一阶段本地事务(开启本地事务的时候,本地事务会对波及到的记录加本地锁)提交之前,确保拿到全局锁。如果拿不到全局锁,就不能提交本地事务,并且一直尝试获取全局锁,直至超出重试次数,放弃获取全局锁,回滚本地事务,开释本地事务对记录加的本地锁。

假如有两个全局事务 gtrx_1 和 gtrx_2 在并发操作库存服务,用意扣减如下记录的库存数量:

AT 实现写隔离过程的时序图如下:

图中,1、2、3、4 属于第一阶段,5 属于第二阶段。

在上图中 gtrx_1 和 gtrx_2 均胜利提交,如果 gtrx_1 在第二阶段执行回滚操作,那么 gtrx_1 须要从新发动本地事务获取本地锁,而后依据 undo_log 对这个 id=10002 的记录进行弥补式回滚。此时 gtrx_2 仍在期待全局锁,且持有这个 id=10002 的记录的本地锁,因而 gtrx_1 会回滚失败(gtrx_1 回滚须要同时持有全局锁和对 id=10002 的记录加的本地锁),回滚失败的 gtrx_1 会始终重试回滚。直到旁边的 gtrx_2 获取全局锁的尝试次数超过阈值,gtrx_2 会放弃获取全局锁,发动本地回滚,本地回滚完结后,天然会开释掉对这个 id=10002 的记录加的本地锁。此时,gtrx_1 终于能够胜利对这个 id=10002 的记录加上了本地锁,同时拿到了本地锁和全局锁的 gtrx_1 就能够胜利回滚了。整个过程,全局锁始终在 gtrx_1 手中,并不会产生脏写的问题。整个过程的流程图如下所示:

2)读隔离(若有全局事务在改 / 写 / 删记录,另一个全局事务对同一记录的读取要被隔离起来,即读写互斥):在数据库本地事务的隔离级别为读已提交、可反复读、串行化时(读未提交不起什么隔离作用,个别不应用),Seata AT 全局事务模型产生的隔离级别是读未提交,也就是说一个全局事务会看到另一个全局事务未全局提交的数据,产生脏读,从前文的第一阶段和第二阶段的流程图中也能够看出这一点。这在最终一致性的分布式事务模型中是能够承受的。

如果要求 AT 模型肯定要实现读已提交的事务隔离级别,能够利用 Seata 的 SelectForUpdateExecutor 执行器对 SELECT FOR UPDATE 语句进行代理。SELECT FOR UPDATE 语句在执行时会申请全局锁,如果全局锁曾经被其余全局事务占有,则回滚 SELECT FOR UPDATE 语句的执行,开释本地锁,并且重试 SELECT FOR UPDATE 语句。在这个过程中,查问申请会被阻塞,直到拿到全局锁(也就是要读取的记录被其余全局事务提交),读到已被全局事务提交的数据才返回。这个过程如下图所示:

四、结束语

XA 协定是 X/Open 提出的分布式事务处理规范。文中提到的 2PC、3PC、TCC、本地事务表、Seata in AT mode,无论哪一种,实质都是事务协调者协调各个事务参与者的本地事务的进度,使使所有本地事务独特提交或回滚,最终达成一种全局的 ACID 个性。在协调的过程中,协调者须要收集各个本地事务的以后状态,并依据这些状态收回下一阶段的操作指令。这个思维就是 XA 协定的要义,咱们能够说这些事务模型恪守或大抵恪守了 XA 协定。

基于消息中间件的最终一致性事务计划是互联网公司在高并发场景中摸索出的一种创新型利用模式,利用 MQ 实现微服务之间的异步调用、解耦合和流量削峰,保障分布式数据记录的最终一致性。它显然不恪守 XA 协定。

对于某项技术,可能存在业界规范或协定,但实践者针对具体利用场景的需要或者出于简便的思考,给出与规范不齐全相符的实现,甚至齐全不相符的实现,这在工程畛域是一种常见的景象。TCC 计划如此、基于消息中间件的最终一致性事务计划如此、Seata in AT mode 模式也如此。而新的规范往往就在这些翻新中产生。

你难道真的没有发现 2.6 节(基于消息中间件的最终一致性事务计划)给出的正确计划中存在的业务破绽吗?请各位从新看下这张图,认真品一品两个微服务的调用方向,把你的想法留在评论区吧 :-)

写在最初

欢送大家关注我的公众号【 惊涛骇浪如码 】,海量 Java 相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。

感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!

正文完
 0