分布式事务

分布式事务

1,概述

1.1,本地事务

本地事务,传统的单机事务,拥有A(atomic)C(consistency)I(isolation)D(durable)原则。

  • atomicity 原子性

在事务执行过程中他们的所有操作是一个整体,类似普朗克常量最小不可分,要么1要么0,一起成功,一起失败(回滚)。

  • consistency 一致性

事务执行必须保证一致性,例如金融领域,A向B转账100元,执行前后银行的总钱数是不变的(不多不少),数据的完整性没有被破坏!(数据库、业务逻辑保证)

  • isolation 隔离性

事务和事务之间互不影响,一个事务中间不会被其他事务感知。数据库保证隔离性的四种不同的隔离级别:

read uncommited (读取未提交内容)

最不安全,不会对事务隔离做任何处理,脏读;一个事务执行时可以读取到另一个事务未提交的数据

read commited(读取提交内容)

一个事务在执行过程中,可以读取到其他事务已提交的内容

repeatable read(可重读,数据库默认级别

一个事务在执行过程中,读取某个数据是一致的,不受其他事务的影响

serializable(可串行化,安全级别最高)

所有的事务排成队,一个个的顺序执行

  • durability 持久性

事务提交后进行持久化,即使机器宕机也可以恢复

1.2,undo和redo

在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log file。

MySQL中的日志文件,有两种和事务有关的日志:undo和redo日志

1.2.1,undo日志

数据库事务具备原子性(atomicity),如果事务执行失败,需要把数据回滚。

事务同时还具备持久性(durability),事务对数据所做的变更就完全保存在数据库,不能因为故障而丢失。

原子性可以利用undo日志来实现。

undo log的原理:为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo log(类似于阿里云ECS快照),然后进行数据的修改,如果出现了错误或者用户执行了rollback语句,系统可以利用undo log中的备份将数据恢复到事务之前的状态。数据库写入数据到磁盘之前,会把数据先缓存到内存中,事务提交时才会写入磁盘中。

用undo log实现atomicity和durability的事务简化流程:

  1. 事务开始
  2. 记录原始值A=1到undo log(内存操作)
  3. 执行修改操作A=3(内存操作)
  4. 记录元素值B=2到undo log(内存操作)
  5. 执行修改操作B=4(内存操作)
  6. 将undo log写入磁盘(真实写入磁盘)
  7. 将数据写入磁盘(真实写入磁盘)
  8. 将事务提交结束
  • 如何保证持久性?

事务提交前,会先把修改数据到磁盘,这个操作本身就是持久化(持久化在事务提交之前完成)

  • 如果保证原子性?

每次对数据库修改,都会把修改前的数据记录在undo log,那么需要回滚时,可以读取undo log恢复数据

若系统在步骤7~8之间奔溃,此时事务并未提交,而undo log已被持久化,后续可以通过undo log回滚数据

若系统在步骤7之前崩溃,此时数据未持久化到磁盘,依然保持之前的状态

  • 缺点

由于我们要多次进行持久化操作(写数据、写undo log),所以效率低

1.2.2,redo日志

和undo log相反,redo log记录的是新数据的备份,在事务提交前,只要将redo log持久化即可,需要立即将数据持久化,减少IO次数

undo+redo事务的简化流程:

  1. 事务开始
  2. 记录原始值A=1到undo log
  3. 修改A=3
  4. 记录修改值A=3到redo log
  5. 记录原始值B=2到undolou
  6. 修改B=4
  7. 记录修改值B=4到redo log
  8. 将undo log写入磁盘
  9. 将redo log写入磁盘
  10. 事务提交结束
  • 如何保证原子性?

如果在事务提交前故障,通过undo log日志恢复数据,如果 undo log还没有写入磁盘,那么数据就未持久化,无需回滚

  • 如何保证持久性?

undo+redo的事务简化流程中没有针对数据库的持久化,因为数据已经写入redo log,而redo log持久化到了硬盘,因为只要到步骤9以后,事务是可以提交的

  • 内存中的修改数据何时持久化到磁盘?

因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存与磁盘的数据一致性),事务提交后也会将内存数据刷入磁盘(也可以按照设定的频率刷新内存数据到磁盘中)

  • redo log何时写入磁盘?

redo log会在事务提交之前,或者redo log buffer 满了的时候也会写入磁盘

问题、疑问

问题1:undo、redo持久化与数据持久化有什么性能差异

undo和redo log持久化是连续内存块的数据写入,减少寻址耗时;由于数据库中的数据都是离散分开的,不能保证更新数据的内存块连续,所以需要耗费大量时间先去寻址,然后才是写入数据

image-20220309223711244

问题2:redo log数据是写入内存buffer中,当buffer满或者事务提交时,将buffer数据写入磁盘,redo log中记录的数据,有可能包含尚未提交的事务,如果此时数据库崩溃,那么数据如何恢复?

两种策略:

  1. 恢复时,只重做已经提交的事务
  2. 恢复时,重做所有事务包括未提交的事务和回滚的事务,然后通过undo log回滚那些未提交的事务

MySQL数据库InnoDB存储引擎使用了B策略, InnoDB存储引擎中的恢复机制有几个特点:

A,在重做Redo Log时,并不关心事务性。 恢复时,没有BEGIN,也没有COMMIT,ROLLBACK的行为。也不关心每个日志是哪个事务的。尽管事务ID等事务相关的内容会记入Redo Log,这些内容只是被当作要操作的数据的一部分。

B,使用B策略就必须要将Undo Log持久化,而且必须要在写Redo Log之前将对应的Undo Log写入磁盘。Undo和Redo Log的这种关联,使得持久化变得复杂起来。为了降低复杂度,InnoDB将Undo Log看作数据,因此记录Undo Log的操作也会记录到redo log中。这样undo log就可以象数据一样缓存起来,而不用在redo log之前写入磁盘了。

包含Undo Log操作的Redo Log,看起来是这样的:

 记录1: <trx1, Undo log insert <undo_insert …>>
 记录2: <trx1, insert …>
 记录3: <trx2, Undo log insert <undo_update …>>
 记录4: <trx2, update …>
 记录5: <trx3, Undo log insert <undo_delete …>>
 记录6: <trx3, delete …>

C,到这里,还有一个问题没有弄清楚。既然Redo没有事务性,那岂不是会重新执行被回滚了的事务?
确实是这样。同时Innodb也会将事务回滚时的操作也记录到redo log中。回滚操作本质上也是对数据进行修改,因此回滚时对数据的操作也会记录到Redo Log中。

一个被回滚了的事务在恢复时的操作就是先redo再undo,因此不会破坏数据的一致性。

记录1: <trx1, Undo log insert <undo_insert …>>     
记录2: <trx1, insert A…>     
记录3: <trx1, Undo log insert <undo_update …>>     
记录4: <trx1, update B…>     
记录5: <trx1, Undo log insert <undo_delete …>>     
记录6: <trx1, delete C…>     
记录7: <trx1, insert C>     
记录8: <trx1, update B to old value>     
记录9: <trx1, delete A>

1.3,分布式事务

分布式事务是指 不是在的那个服务或者单个数据库架构下,产生的事务:

  • 跨数据源的分布式事务
  • 跨服务的分布式事务
  • 综合情况

1.3.1,跨数据源

随着业务数据规模的快速发展,数据量越来越大,单库单表主键成为瓶颈。所以我们对数据库进行水平拆分,将原有的单库单表拆分成数据库分片,于是产生了跨数据库事务问题。

image-20220309230531045

1.3.2,跨服务

在业务发展初期,“一块大饼”的单业务系统架构,能满足基本的业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,单系统架构逐渐成为业务发展瓶颈,解决业务系统的耦合、可伸缩问题的需求越来越强。

如图所示,按照面向服务(SOA,Service-Oriented Architecture)架构的设计原则,将单个业务系统拆分成多个业务系统,降低各个系统的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。

image-20220309231251935

1.3.3,分布式系统的数据一致性问题

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。在分布式网络环境下,我们无法保证所有服务、数据库都有百分百可用,一定会出现部分服务、数据库执行成功,另一部分执行失败的问题。

当出现部分业务操作成功,部分业务操作失败时,业务数据就会出现不一致。

例如电商行业中比较场常见的下单付款案例,包括下面几个行为:

  • 创建新订单
  • 扣减商品库存
  • 从用户账户余额扣除金额

完成上述操作需要访问三个不同的微服务和数据库(每个事务保证ACID)

image-20220309231711355

在分布式环境,肯定会出现部分操作成功,部分操作失败的问题。

但是,当我们把三件事情看做一个事情时,要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务。

2,解决分布式事务的思路

首先需要理解CAP定理和BASE理论

2.1,CPA定理

image-20220309232727011

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统的三个指标

  • consistency (一致性)
  • availability (可用性)
  • partition tolerance (分区容错性)

一个分布式系统是无法同时满足上述指标的三点!最多满足2点

2.1.1,partition tolerance

大多数的分布式系统都分布在多个子网络,每个网络都是一个区(partition)。分区容错的意思是区间通信可能失败。比如,一台服务器放在上海,另一台放在北京,这就是2个区,他们之间可能因为网络问题无法通信。

一般来说,分布式系统,分区容错无法避免,因此可以认为CAP的P总是成立的,根据剩下的C和A无法同时做到!

2.1.2,consistency

写操作之后的读操作,必须返回该值。例如:某条记录v0,用户向G1发起写操作,将其改为v1,

image-20220309233744300

接下来用户读操作就会得到v1,这个就是一致性。

image-20220309233753692

如果使用的是分布式系统,可能读写操作时分离的,写操作时连接G1,读操作连接G2,如果G2没有同步v1,那么返回的还是历史数据v0,这个就不能满足一致性

image-20220309233856569

为了保证数据一致性,就要在G1写操作后,让G1发送消息给G2,要求G2也修改数据v1,这样就满足了一致性。

image-20220309234047977

2.1.3,availability

可用性,意思是只要收到客户请求,服务器就必须给出回应(对错不论)。

用户可以选择向G1或者G2发起读操作,不管那台服务器,只要收到请求,就必须返回给用户数据(不能卡住),否则就不满足可用性

2.1.4,consistency和availability矛盾

G1更新数据后向G2同步,这段时间用户给G2发起的请求

  1. 如果要保证一致性,就必须等G2同步数据完成,才能返回一致性的数据,这时就不满足可用性
  2. 如果要保证可用性,在未同步完成之前,请求过来就必须把不一致的数据返回给用户,不能保证一致性

2.1.5,疑问

  • 怎么才能同时满足CA?

除非之前的单点架构,分布式架构是无法满足的

  • 何时要满足CP?

对一致性要求高的场景,例如金融转账,zookeeper就是这样的,在服务节点间数据同步时,服务对外不可用

  • 何时满足AP?

对可用性要求较高的场景。eureka,必须保证注册中心随时可用,不然拉取不到服务就可能出问题(有可能服务挂了,但是还没有感知出来)

2.2,BASE理论

BASE是三个单词的缩写:

  • basically availability(基本可用)

保证数据的可用性,任何请求都会有响应(也可能是失败)。

  • soft state(软状态)

软状态和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据传输的过程存在延时。

  • eventually consistency(最终一致性)

强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

我们解决分布式事务,就是根据上述理论来实现的

以网购下单、减库存、扣款为例:

订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分

  • cp方式:现在如果要满足事务的强一致性,就必须在订单服务数据库锁定的同时,对库存服务、用户服务数据资源同时锁定。等待三个服务业务全部处理完成,才可以是否资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就满足了CP
  • AP方式:三个服务对于数据库各种独立执行自己的业务,执行本地事务,不要求互相锁定资源。但是这个中间状态下,我们去访问数据库,可能遇到数据不一致的情况,不过我们需要做一些后补措施,保证在经过一段时间后,数据最终满足一致性(高可用、弱一致)

由上面的两种思想,衍生出很多的分布式事务解决方案:

  • XA
  • TCC
  • 可靠消息最终一致
  • AT

2.3,解决思路

2.3.1,分阶段提交

DTP和XA

分布式事务的解决手段之一,就是两阶段提交协议(2PC: Two-Phase commit),那么到底上面是两阶段提交协议呢?

1994年,X/OPEN组织(即现在的Open Group)定义了分布式事务处理的DTP模型,该模型包括这样几个角色:

  • 应用程序(AP,application):我们自己的微服务
  • 事务管理器(TM,transaction management):全局事务管理者
  • 资源管理器(RM,resource management):一般是数据库
  • 通信资源管理器(CRM,connection resource management):是TM和RM的通信中间件

在该模型中,一个分布式事务(全局事务)可以拆分成多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功。若有一个本地事务失败,则所有的事务都必须回滚。但问题是,本地事务处理过程中,并不知道其他事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。

因此,各个本地事务的通信必须有统一的标准,否则不同的数据库间就无法通信。XA就是X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

二阶段(2PC)

二阶段提交协议就是根据上述思想衍生出来的,将全局事务拆分为两个阶段来执行:

  • 阶段一:准备阶段,各个本地事务完成本地事务的准备工作
  • 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行事务提交或回滚

这个过程中需要一个协调者(coordinate,有点类似项目经理),还有事务参与者(voter,我们这些研发)

1)正常情况(成功流程)

image-20220310193745942

投票阶段:协调者询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务写入redo和undo日志,然后反馈给事务执行成功的信息(agree)

提交阶段:协调组发现每个事务参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务

2)异常情况(失败回滚)

image-20220310194028943

投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务写入redo和undo日志,然后反馈事务执行结果,但只要有一个参与者返回的事务disagree,则说明事务执行失败

提交阶段:协调组发现有一个或多个参与者返回的是disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。

3)优点与缺陷

优点:保持强一致(分布式强一致的特性必定带来可用性变差)、比较成熟

缺点:单点故障、资源锁定

二阶段提交的问题

  • 单点故障(由于协调者的重要性,一旦协调者发生故障)

2PC的缺点在于不能处理fail-stop形式的节点failure,加锁coordinate和voter3都在commit阶段宕机,而voter1和voter2没有收到commit消息,这个时候voter1和voter2就原地抓头?因为他们并不能判断现在情况是2个场景中的哪一个:

  1. 上轮全票通过然后voter3第一个收到commit的消息并在commit操作之后crash
  2. 上轮voter3反对所有干脆没有通过
  • 阻塞问题(资源锁定太长)

在准备阶段、提交阶段,每个事务参与者都会锁定本地资源,并等待其他事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了

image-20220310194623481

面对二阶段提交的上述缺点,后来又演变出了三阶段,但是依然没有完全解决阻塞和资源锁定的问题,而且引入新的问题,因此使用场景较少!

三阶段(3PC)

三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交

与二阶段的区别:

  1. 对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。
  2. 在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

缺点:

如果进入PreCommit后,Coordinator发出的是abort请求,假设只有一个voter收到并进行了abort操作,而其他对于系统状态未知的voter会根据3PC选择继续Commit,此时系统状态发生不一致性。

2.3.2,TCC

TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。

它本质是一种补偿的思想。事务运行过程包括3个方法:

  • try:资源的检测和预留
  • confirm:执行的业务操作提交;要求try成功confirm一定要能成功
  • cancel:预留资源释放

执行分为2个阶段:

  • 准备阶段(try):资源的检测和预留
  • 执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm。反之,执行cancel

image-20220310202330789

粗略看似乎和2阶段没有什么区别,其实差别很大:

  • try,confirm,cancel都是独立的事务,不受其他参与者的影响,不会阻塞其他人
  • try,confirm,cancel由程序员在业务层面编写,锁颗粒度由代码控制

实例

以下单业务中的扣款余额为例来看三个不同的方法要怎么编写,假设账户A原有余额是100,需要余额扣减30元。

image-20220310202601062

  • 一阶段(try):余额检测,冻结用户部分金额,此阶段执行完毕,事务已经提交

    • 检查用户余额是否充足,如果充足,冻结部分余额
    • 在账户表中添加冻结金额字段,值为30,余额不变
  • 二阶段:

    • 提交(confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额情况

      • 修改冻结金额为0,修改余额为100-30=70
    • 补偿(cancel):释放之前冻结的金额,并非回滚

      • 余额不变,修改账户冻结金额为0

优点与缺点

优点:

  • 每一个阶段都会提交本地事务并且释放锁,并不需要等待其他事务的执行结果
  • 如果其他事务执行失败,最后不回滚,而是执行补偿操作,这样避免了长期锁定与阻塞等待,执行效率比较高,属于性能比较好的分布式事务

缺点:

  • 代码侵入:需要认为编写代码实现try、confirm、cancel逻辑
  • 开发成本高(复杂):一个业务需要拆分成3个步骤,分别编写业务实现,业务逻辑复杂
  • 安全性考虑:cancel动作如果执行失败,资源就无法释放,需要引入重试机制,而重试可能导致重复执行,还要考虑重试时的冥等问题

场景

  • 对事务有一定的一致性要求(最终一致性)
  • 对性能要求较高
  • 开发人员具备较高的编码能力和冥等处理经验

2.3.3,可靠消息服务

这种实现方式的思路其实是ebay,其基本的设计思想是将远程分布式事务拆分成一系列的本地事务

基本原理

一般分为事务的发起者和其他参与者

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者(前提条件:发消息一定能成功)
  • 事务参与者B接收到消息后执行本地事务

如图(rabbitmq有失败重试机制和死信队列):

image-20220310204651634

过程就像去学校食堂吃饭:

  • 去窗口点一份兰州拉面,收银员先扣钱
  • 叫拉面师傅去做面,我先回到餐厅找个位置坐下玩手机或者看书(不锁定此时资源)
    • 拉面师傅耗时一会儿后,面做好了,叫我去端面
    • 拉面师傅发现原材料不够了(重试)

几个注意事项:

  • 事务发起者A必须保证本地事务成功后,消息一定能够发送成功
  • MQ必须保证消息正确投递与持久化保存
  • 事务参与者B必须确保消息的最终一定能消费,如果失败需要多次重试
  • 事务B执行失败,会重试;但不会导致事务A回滚

那么问题来了,我们如何保证消息发送一定成功?如何保证消费者一定能收到消息?

本地消息表

为了避免消息发送失败或者丢失,我们可以把消息持久化到数据库中。实现有简化版和解耦版两种方式。

1)简化版本

image-20220310210253565

事务发起者

  1. 开启本地事务
  2. 执行事务相关业务
  3. 发送消息到MQ
  4. 把消息持久化到数据库,标记为已发送
  5. 提交本地事务

事务接收者:

  1. 接收消息
  2. 开启本地事务
  3. 处理相关业务
  4. 修改数据库消息状态为已消费
  5. 提交本地事务

额外定时任务:

  1. 定时扫描表中超时未消费消息,重新发送

优点:

  • 与TCC相比,实现相对简单,开发成本地

缺点:

  • 数据一致性完全依赖于消息服务,因此消息服务必须可靠
  • 需要处理被动业务方的冥等问题
  • 被动业务失败不会导致主动业务的回滚,而是重试被动的业务
  • 事务业务与消息发送业务耦合,业务数据与消息表要在一起

独立消息服务

为了解决本地消息表的缺点,我们会引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概的模型如下:

image-20220310213018687

时序图

image-20220310213428185

事务发起者A的基本执行步骤:

  1. 开启本地事务
  2. 通知消息服务,准备发送消息(消息服务将消息持久,标记为准备发送)
  3. 执行本地业务
  4. 定时任务:定时扫描数据库中状态值确认发送的消息,然后询问对于的事务发起者,事务业务是否执行成功,结果:
    • 业务执行成功:尝试发送消息,成功后修改状态值为已发送
    • 业务执行失败:把数据库消息状态修改为取消

事务参与者B的基本执行步骤:

  1. 接收消息
  2. 开启本地事务
  3. 执行业务
  4. 通知消息服务,消息已经接收和处理
  5. 提交事务

优点:

  • 解除了事务业务与消息相关业务的耦合

缺点:

  • 实现起来比较麻烦

rocketmq事务消息

rocketMQ本身自带了事务消息,可以保证消息的可靠性,原理其实就是自带了本地消息表,与我们上面讲的思路类似

rabbitmq消息确认

rabbitmq确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息确认机制:

生产者确认机制:确保消息从生产者到达MQ不会有问题

  • 消息生产者发送消息给rabbitmq时,可以设置一个异步的监听器,监听来着MQ的ACK

  • MQ接收到消息后,会返回一个回执给生产者

    • 消息到达交换机后路由失败,会返回失败ACK
    • 消息路由成功,持久化失败,会返回失败ACK
    • 消息路由成功,持久化成功,会返回成功ACK
  • 生产者提前表写好不同回执的处理方式

    • 失败回执:等待一定时间后重新发送
    • 成功回执:记录日志等行为
  • 消费者确认机制:确保消息能被消费者正确消费

    • 消费者需要监听队列的时候指定手动ACK模式(try、catch、final中手动执行ACK)
    • rabbitmq把消息投递给消费者后,会等待消费者ACK,接收到ACK后才删除消息,如果没有接收到ACK消息会一直保留在服务端,如果消费者断开连接或异常后,消息会投递给其他消费者
    • 消费者处理完消息,提交事务后,手动ACK。如果执行过程中抛出异常,则不会

接触到的代码业务逻辑中xxx删除、xx删除就是使用的这种分布式事务(满足AP)。

  1. 用户在门户A组件发起了删除事务(页面入口),门户A组件直接透传请求组件B(组件B是删除操作的真实发起者)
  2. 组件B去数据库的事务记录表存储相关删除记录(状态值置为【0释放中】)并通过组件C查询相关需要配合删除的资源信息
  3. 组件B获取到需要协同删除的资源列表后,给各个资源组件发送rabbitmq消息,成功后将事务记录表(状态值置为1【1消息发送成功】),同时在资源释放表中记录相关资源信息并将状态值置为【0释放中】,组件B的本地事务结束
  4. 其他资源释放组件接收到组件B的释放消息后,开始执行资源释放事务业务逻辑,执行成功\失败后直接调用组件B的回调接口,返回相关释放详情
  5. 组件B的资源释放回调接口会将各个资源的释放情况记录在资源释放表,如果成功则将状态值置为【1资源释放成】,如果没有返回或者返回失败状态值不变
  6. 组件B有一个延时操作,过了大概1分钟去查询资源释放表,如果全部成功就将事务记录表中的状态值置为【2成功】,如果有失败的就直接跳过(需要人工介入
  7. 每天有一个凌晨的定时任务去重试那些释放失败的资源

消息事务的优缺点:

优点:

  • 业务相对简单,不需要编写三阶段业务
  • 是多个本地事务的结合,因此资源锁定周期短,性能好

缺点:

  • 代码侵入
  • 依赖MQ的可靠性
  • 消息发起者可以回滚,但是消息参与者无法引起事务回滚
  • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况

针对事务无法回滚的问题,有人提出说可以在事务参与者执行失败后,再次利用MQ通知消息,然后由消息通知其他参与者回滚。(那这个本质不就是一个2PC模型,无非这个不锁资源)

2.3.4,AT模式

2019年1月份,seata开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。可以看做对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

在AT模式下,用户只需要关注自己的“业务SQL",用户的业务SQL作为一阶段,seata框架会自动生成事务的二阶段提交与回滚操作。

原理

image-20220310220606857

基本流程:

  • 第一阶段:组件A直接执行事务,各个组件收到事务问题
  • 第二阶段:发起方检查是否需要回滚,
    • 如果都成功,那就不用额外执行步骤4
    • 如果有失败的,直接回滚数据(但是这个回滚数据、比对数据都是seata自动完成的

AT模式底层做的事情与TCC模式不一样,第二阶段不需要我们自己去编写代码,全部有seata自己实现了:我们写的代码与本地事务时的代码是一样的,无需手动手动处理分布式事务

底层实现原理:

  1. 我们执行事务的业务逻辑时,更新一条语句 update money = 100 from user where id =1;
  2. seata会拦截我们执行的sql语句并解析,然后查询一下原始数据 select * from user where id =1; before image 类似于undo日志
  3. 放行业务sql执行
  4. 待业务sql执行结束,再次查询一下select * from user where id =1; after image 类似于redo日志
  5. 后续如果所有事务都成功,直接删除undo和redo日志;如果有事务失败需要回滚,先比较redo数据是不一致的,如果是才回滚,不是就要人工介入(概率很低)

image-20220310223839148

image-20220310223916750

由于上述步骤5的存在,所以AT同时兼具分阶段提交和TCC的优点,同时也因为步骤5需要额外拦截解析业务SQL所以需要消耗一定的性能

3,seata框架

3.1,AT实现原理

seata的AT模式中有这样的结果概念:

  • TC(transaction coordinate):事务协调者,维护全局和分支事务的状态,驱动全局事务提交与回滚(TM之间的协调者)
  • TM(transaction management):事务管理器,定义全局事务的范围,开始全局事务、提交或回滚全局事务
  • RM(resource management):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交与回滚

image-20220310224526161

  • TM:业务模块中全局事务的开启者

    • 向TC开启一个全局事务
    • 调用其他微服务
  • RM:业务模块执行者中包含有RM部分,负责向TC汇报事务执行状态

  • TM:结束对微服务的调用,通知TC,全局事务执行完毕,(事务一阶段结束)

  • TC:汇总各个分支事务执行结果,决定分布式事务是提交还是回滚

  • TC:通知所有RM提交/回滚资源,事务二阶段结束

一阶段

  • TM开启全局事务,并向TC声明全局事务,包括全局事务XID信息

  • TM所在服务调用其他微服务

  • 微服务,主要有RM来执行

    • 查询before_image
    • 生成undo_log并写入数据库
    • 执行本地事务
    • 查询after_image
    • 生成redo_log并写入数据库
    • 向TC注册分支事务,告知事务执行结果
    • 获取全局锁(阻止其他全局事务并发修改当前数据)
    • 释放本地锁(不影响其他业务对数据的操作)
  • 待所有业务执行完毕,事务发起者(TM)会尝试向TC提交全局事务

二阶段

  • TC统计分支事务执行情况,根据结果判断下一步行为

    • 所有分支都成功:通知分支事务,提交事务
    • 有分支执行失败:通知执行成功的分支事务,回滚数据
  • 分支事务的RM

    • 提交事务:直接清空before_image、after_image信息,释放全局锁
    • 回滚事务:
      • 校验after_image,是否脏写
      • 如果没有脏写,回滚数据到before_image,清除before_image和after_image
      • 如果有脏写,请求人工介入

3.2,其他模式

TCC模式

(try、confirm、cancel)

Saga 模式

3.2,样例

  1. 搭建一个TC服务,事务的协调者,负责各个事务的通信
  2. 在每个事务的参与者中引入RM/TM的包
  3. 告知每个事务的参与者,TC的地址在哪里
  4. 标记事务的范围
nohup sh seata-server.sh >> output.log 2>&1 &

参考链接:

  1. 分布式事务全攻略
  2. 对分布式事务及两阶段提交、三阶段提交的理解
  3. 分布式幂等问题解决方案三部曲
  4. https://github.com/seata/seata
  5. https://seata.io/zh-cn/docs/overview/what-is-seata.html

posted on 2022-03-10 00:10  周健康  阅读(178)  评论(0编辑  收藏  举报

导航