分布式事务

1. 概念

1.1 事务的概念

首先谈下数据库的事务database transaction
数据库的事务满足如下条件 原子性 一致性 隔离性和持久性 简称ACID
Atomic(原子性) : 事务必须是原子的工作单元,简单来说就是事务内的所有操作要么都执行,要么都不执行。
Consisitent(一致性):事务在完成的时候必须是所有的数据都保持一致。可以理解为数据的完整性约束,就是不存在中间状态的数据。如张三原本有100块,李四0块,张三给李四转账了40,那么张三应该是60,李四应该是40,不存在张三扣款,李四钱还没到账的中间状态。
Isolation(隔离性):并发事务所做的修改必须和其他事务所做的修改是隔离的。指多个事务并发执行的时候不会互相干扰,一个事务内部的数据对于其他事务是隔离的。
Duration(持久性): 事务完成之后,对系统的影响是永久性的,一个事务完成之后数据就被永远保存下来,之后其他的操作或者故障都不会对事务的结果产生影响。

通俗和便于理解的角度上解释就是事务的目的就是为了让事务内的更新要么都成功要么都失败。

1.2 mysql事务

mysql事务的处理过程
redo和undo文件
redo是记录事务修改后的数据
undo是记录事务修改之前的数据,保证可以回滚

1)记录redo和undo log文件,确保日志在磁盘上持久化
2)更新数据记录
3)提交事务,redo写入commit记录
4)清理undo文件和释放锁

2. 分布式事务及其解决方案

分布式事务
1)分库分表
在这里插入图片描述
2)SOA化
在这里插入图片描述
如上图:电商下单一件商品,触发的就是库存中心某个商品数量-1,与此同时订单中心增加记录,这两个步骤必须要是原子性的。
一定要保证,下单和订单这两个合并起来,要么全部成功要么全部失败。

2.1 经典的 X/OpenDTP 事务模型

X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是定义了规范和 API 接口,由各个厂商进行具体的实现。这个标准提出了使用二阶段提交(2PC – Two-Phase-Commit)来保证分布式事务的完整性。后来 J2EE 也遵循了 X/OpenDTP 规范,设计并实现了 java 里的分布式事务编程接口规范-JTA
在这里插入图片描述
先看下
X/OpenDTP的各个角色
AP:应用程序,也就是业务层。哪些操作属于一个事务,就是 AP 定义的。
RM:资源管理器。一般是数据库,也可以是其他资源管理器,比如数据库,消息队列,文件系统。
TM:事务管理器、事务协调者,负责接收来自用户程序(AP)发起的 XA 事务指令,并调度和协调参与事务的所有 RM(数据库),确保事务正确完成。

在分布式系统中,每一个机器节点虽然都能够明确知道自己在进行事务操作过程中的结果是成功还是失败,但却无法直接获取到其他分布式节点的操作结果。因此当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的 ACID 特性,就需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为 AP。TM 负责调度 AP 的行为,并最终决定这些 AP 是否要把事务真正进行提交到(RM)。

完成事务操作主要有以下几个步骤:

  1. 参与分布式事务的应用程序(AP)先到 TM 上注册全局事务。
  2. 然后各个 AP 直接在相应的资源管理器(RM)上进行事务操作。
  3. 操作完成以后,各个 AP 反馈事务的处理结果给到 TM。
  4. TM 收到所有 AP 的反馈以后,通过数据库提供的 XA 接口进行数据提交或者回滚操作。

2.2 2PC

2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
注意这只是协议或者说是理论指导,只阐述了大方向,具体落地还是有会有差异的。
让我们来看下两个阶段的具体流程。
在这里插入图片描述

2.2.1 2PC的过程

阶段一: 提交事务请求
这一阶段也叫预事务

  1. TM 向所有的AP发送事务内容
    询问是否执行事务的提交操作,并且等待各个AP响应
  2. 执行事务
    执行事务各个 AP 节点执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中,尽量把提交过程中所有消耗时间的操作和准备都提前完成确保后面 100%成功提交事务
  3. 各个 AP 向 TM 反馈事务询问的响应如果各个 AP 成功执行了事务操作,那么就反馈给 AP yes 的响应,表示事务可以执行;如果 AP 没有成功执行事务,就反馈给 TM no 的响应,表示事务不可以执行。

上面这个阶段有点类似 TM 组织各个 AP 对一次事务操作的投票表态过程,因此2pc 协议的第一个阶段称为“投票阶段”,即各 AP 投票表名是否需要继续执行接下去的事务提交操作。

阶段二:执行事务提交
在这个阶段,TM 会根据各 AP 的反馈情况来决定最终是否可以进行事务提交操作,正常情况下包含两种可能,假如 TM 从所有参与者获得的反馈都是 yes 响应,那么就会执行事务提交。

  1. 发送提交请求:TM 向所有 AP 节点发出 commit 请求
  2. 事务提交AP 接收到 Commit 请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源
  3. 反馈事务提交结果:AP 在完成事务提交之后,向 TM 发送 Ack 消息
  4. 完成事务:TM 接收到所有 AP 反馈的 ack 消息后,完成事务

事务回滚流程
如果第一个阶段中的某一个资源预提交失败,那么第二个阶段就回滚第一阶段已经预提交成功的资源假设任何一个 AP 向 TM 反馈了 NO 的响应,或者在等待超时之后,TM 无法接收到所有 AP 的反馈响应,那么就会中断事务

  1. 发送回滚请求:TM 向所有 AP 发出 abort 请求
  2. 事务回滚:AP 收到 abort 请求后,会利用在第一阶段记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源
  3. 反馈事务回滚结果各 AP 在完成事务回滚之后,向 TM 发送 Ack 消息
  4. 中断事务:TM接收到所有 AP 反馈的 ack 消息后,完成事务中断。

二阶段提交将一个事务的处理过程分为投票和执行两个阶段. 二阶段提交的优点在于,它充分考虑到了分布式系统的不可靠因素,并且采用非常简单的方式(两阶段提交)就把由于系统不可靠从而导致事务提交失败的概率降到最小

假如一个事务的提交过程总共需要 30 秒的操作,其中 prepare 阶段需要 28 秒(主要是确保事务日志落地磁盘等各种耗时的 I/O 操作),真正的 commit 阶段只需要花费两秒,那么 Commit 阶段发生错误的概率与 Prepare 阶段相比,只是它的2/28(<10%),也就是说,如果 Prepare 阶段成功了,则 Commit 阶段由于时间非常端,失败概率小,会大大增加分布式事务成功的概率。

2.2.2 2PC的问题

首先说下2PC的优点:原理简单,实现方便。

说完优点就要说缺点了,下面以问题来说明缺点

2.2.2.1 同步阻塞问题

第一阶段会返回ack来通知预事务是否成功与否,那么第二阶段提交失败呢?
这边有两种情况:
第一种是回滚操作: 不断重试直到所有的参与者都回滚了,否则第一阶段成功的参与者会一直阻塞。
第二种是提交操作:也是不断重试,直到所有都提交成功。

2PC是同步阻塞协议,第一阶段事务协调者会等待所有的参与者响应了才会进行下一步操作,但是第一阶段的协调者是有超时机制的,假设因为网络因素没有收到某参与者的响应或者参与者挂了,超时后会判断此事务失败,向所有的参与者发送回滚命令。

由于第二阶段没有超时机制,所以只能不断重试!

上述的内容就说明了2PC的第一个缺陷,同步阻塞,造成性能损耗。

协调者是一个单点,存在单点故障问题

2.2.2.2 协调者单点故障

协调者是一个单点,所以存在单点故障问题。
步骤是 发准备-接受响应-发commit
所以由如下情况:
1)发准备前宕机了,等于事务还没开始
2)发完准备宕机了,那么有些参与者等于都执行了处于事务资源锁定的状态,不仅仅事务无法执行,还会因为锁定了某些公共资源而阻塞系统的其他操作
3)发送回滚或者提交事务命令前宕机,无法执行事务,第一阶段准备成功参与者都阻塞
4)发送回滚或者提交事务命令后方剂,命令发送,大概率会提交或者回滚成功,资源释放,但是如果出现网络分区,那么有些参与者就收不到命令而阻塞

协调者故障一般需要通过选举来的到新的协调者【如zk选主一般】
如果处于第一阶段,因为事务还没正式commit,所以直接回滚就好了。
如果处在第二阶段,假定参与者都没挂,新的协调者可以向所有参与者确认他们情况来决定下一步动作。

如果个别参与者挂了,就很麻烦了,为了方便书写,一下就以leader和follower来辨识协调者和参与者。

假定leader发送了commit,然后followerA收到了信息并且执行了,然后leader和followerA都挂了,其他的follower没收到这个commit命令,新的leader被选举出来,此时就出现followerA和其他follower数据不一致的问题,每个follower的状态只有自己和leader才知道。
新的leader无法通过follower来判断挂掉的节点是什么状态。

如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。

如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。

所以说极端情况下还是无法避免数据不一致问题。

2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

当然具体的实现可以变形,而且 2PC 也有变种,例如 Tree 2PC、Dynamic 2PC。

还有一点不知道你们看出来没,2PC 适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传一张图片或者发送一条短信。

而且像 Java 中的 JTA 只能解决一个应用下多数据库的分布式事务问题,跨服务了就不能用了。

简单说下 Java 中 JTA,它是基于XA规范实现的事务接口,这里的 XA 你可以简单理解为基于数据库的 XA 规范来实现的 2PC。(至于XA规范到底是啥,篇幅有限,下次有机会再说)

接下来我们再来看看 3PC。

2.3 3PC

3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
3PC 协议主要用来解决 2PC 的同步阻塞问题的一种优化方案,3pc 分为 3 个阶段分别为:cancommit、Precommit、doCommit。和 2 阶段提交的区别在于:

(1) 在协调者和参与者中引入了超时机制,2pc 只有在协调者拥有超时机制,协调者在一定时间内没受到参与者的信息则默认为失败;

(2) 把 2 阶段提交的第一个阶段拆分成了两个步骤。
cancommit 阶段:协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 的响应,否则返回 No 的响应。这一阶段主要是确定分布式事务的参与者是否具备了完成commit 的条件,并不会执行事务操作。

  1. 询问参与者是否可以执行事务提交操作。
  2. 正常情况下只要能够顺利执行事务,就返回 yes 的响应,并进入预备状态。
    在这里插入图片描述
    precommit 阶段:事务协调者根据参与者的反馈情况来决定是否继续执行事务的 precommit 操作,在这一个阶段,会有两种可能性,第一种是,在 cancommit 阶段所有参与者都反馈的是 yes,则会进行事务预执行。

1.协调者向参与者发送 precommit 请求。
2.参与者收到 precommit 请求后,执行事务操作,并把事务的 undo 和 redo 信息记录到事务日志中
3.返回事务的执行结果给到协调者,并等待最终的提交指令如果任意一个事务参与者在第一阶段返回了 no,则执行事务中断请求。
3.1. 向所有事务参与者发送事务中断请求。
3.2. 对于事务参与者来说,无论是收到协调者的中断请求,还是等待协调者新的指令之前出现超时,参与者都会中断事务。
在这里插入图片描述
doCommit 阶段:这个阶段同样存在两种情况,正常情况下,precommit 都响应了 ack 给到协调者,那么协调者会发起事务提交请求。
1) 协调者向所有参与者发送 docommit 请求。
2)参与者收到 docommit 请求后,执行事务提交操作,并释放所有事务资源。
3)事务提交以后返回 ack 给到协调者。
4) 协调者收到所有参与者的响应后,完成事务。
如果在 precommit 阶段,有参与者没有发送 ack 给到协调者,那么则执行事务中断指令。
1)协调者向所有参与者发送中断事务的请求。
2)参与者收到请求以后,利用在第二个阶段记录的 undo 信息来执行事务回滚操作。
3) 向协调者发送 ack 消息,协调者收到消息以后,执行事务中断操作。
在这里插入图片描述
以上三个阶段任何一个阶段返回false,都会宣布事务失败。
准备阶段变更不会直接执行事务,只是询问参与者是否有条件来接事务,因此不会一上来就锁资源,使某些资源不可用的情况下所有参与者阻塞。
预提交阶段引入起到了统一状态的作用,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。

如果是某个节点,当知道自己是预提交状态,说明,其他参与者也进入了预提交状态。但是多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次

我们知道 2PC 是同步阻塞的,上面我们已经分析了协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着。

那么引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。

然而超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。

3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。

新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。

所以说 3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。

但是这也只能让协调者知道该如果做,但不能保证这样做一定对,这其实和上面 2PC 分析一致,因为挂了的参与者到底有没有执行事务无法断定。

所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。

让我们总结一下, 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。

所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。

2.4 TCC

TCC是Try-Confirm-Cancel, 比如在支付场景中,先冻结一笔资金,再去发起支付。如果支付成功,则冻结资金进行实际扣除;如果支付失败,则取消资金冻结

TCC-Try-Confirm-Cancel
Try 指的是预留,即资源的预留和锁定, 注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。

其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚

流程还是很简单的,难点在于业务上的定义,对于每一个操作你都需要定义三个动作分别对应Try - Confirm - Cancel。

因此 TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。

还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。

相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。

2.5 本地消息表

本地消息表其实是利用了各系统本地的事务来实现分布式事务。
用一张数据表来存放本地消息,执行业务时把业务执行和将消息放入消息表操作放在同一个事务中,这样保证消息放到本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况

2.6 消息事务

RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。

第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。

并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。

如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。

如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。

可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
在这里插入图片描述

2.7 最大努力通知

其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。

就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。

事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。

所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。

适用于对时间不敏感的业务,例如短信通知。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值