分布式事务

前言

对于网上购物的每一笔订单来说,电商平台一般都会有两个核心步骤:一是订单业务采取下订单操作,二是库存业务采取减库存操作。

通常,这两个业务会运行在不同的机器上,甚至是运行在不同区域的机器上。针对同一笔订单,当且仅当订单操作和减库存操作一致时,才能保证交易的正确性。也就是说一笔订单,只有这两个操作都完成,才能算做处理成功,否则处理失败,充分体现了“All or nothing”的思想。

在分布式领域中,这个问题就是分布式事务问题。

什么是分布式事务

学过数据库的人都知道什么是事务:

事务(Transaction)提供一种机制,将包含一系列操作的工作序列纳入到一个不可分割的执行单元。只有所有操作均被正确执行才能提交事务;任意一个操作失败都会导致整个事务回滚(Rollback)到之前状态,即所有操作均被取消。简单来说,事务提供了一种机制,使得工作要么全部都不做,要么完全被执行,即 all or nothing。

事务具备四大基本特征ACID,即原子性、一致性、隔离性以及持久性。

通常情况下,我们所说的事务都是本地事务,也就是单机上的事务。而分布式事务,则是在分布式系统中运行的事务,由多个本地事务组合而成。在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。文章开头提到的电商处理订单问题,就是典型的分布式事务。

如何实现分布式事务

实际上,分布式事务主要是解决在分布式环境下,组合事务的一致性问题。实现分布式事务有以下 3 种基本方法:

  • 基于 XA 规范的二阶段提交协议方法;
  • 三阶段提交协议方法;
  • 基于消息的最终一致性方法。

其中,基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法,采用了强一致性,遵从 ACID。基于消息的最终一致性方法,采用了最终一致性,遵从 BASE 理论。(关于ACID与BASE理论在后面的知识扩展中有简单的介绍)

基于 XA 规范的二阶段提交方法

什么是XA

提到 XA 规范,就不得不说 DTP 模型( Distributed Transaction Processing),因为 XA 规范约定的是 DTP 模型中 2 个模块(事务管理器和资源管理器)的通讯方式。

在这里插入图片描述

DTP模型中各模块的作用如下:

  • AP:应用程序(Aplication Program),一般指事务的发起者(比如数据库客户端或者访问数据库的程序),定义事务对应的操作(比如更新操作 UPDATE executed_table SET status = true WHERE id=100)。
  • RM:资源管理器(Resource Manager),管理共享资源,并提供访问接口,供外部程序来访问共享资源,比如数据库,另外 RM 还应该具有事务提交或回滚的能力。
  • TM:事务管理器(Transaction Manager),TM 是分布式事务的协调者。TM 与每个 RM 进行通信,协调并完成事务的处理。

整个过程可以这样理解:应用程序访问、使用资源管理器的资源,并通过事务管理器的事务接口(TX interface)定义需要执行的事务操作,然后事务管理器和资源管理器会基于 XA 规范,执行二阶段提交协议。

那么,XA 规范是什么样子的呢?它约定了事务管理器和资源管理器之间双向通讯的接口规范,并实现了二阶段提交协议:

在这里插入图片描述

整个过程的具体步骤如下:

  1. AP(应用程序)联系 TM(事务管理器)发起全局事务;
  2. TM 调用 ax_open() 建立与资源管理器的会话;
  3. TM 调用 xa_start() 标记事务分支(Transaction branch)的开头;
  4. AP 访问 RM(资源管理器),并定义具体事务分支的操作,比如更新一条数据记录(UPDATE executed_table SET status = true WHERE id=100)和插入一条数据记录(INSERT into operation_table SET id = 100, op = ‘get-cdn-log’);
  5. TM 调用 xa_end() 标记事务分支的结尾;
  6. TM 调用 xa_prepare() 通知 RM 做好事务分支提交的准备工作,比如锁定相关资源,也就是执行二阶段提交协议的提交请求阶段;
  7. TM 调用 xa_commit() 通知 RM 提交事务分支(xa_rollback() 通知 RM 回滚事务),也就是执行二阶段提交协议的提交执行阶段;
  8. TM 调用 xa_close() 关闭与 RM 的会话。

xa_start() 和 xa_end() 在准备和标记事务分支的内容,然后调用 xa_prepare() 和 xa_commit()(或者 xa_rollback())执行二阶段提交协议,实现操作的原子性。

二阶段提交如何保证分布式事务一致性

前面我们已经了解了什么是XA,同时也应该注意到XA规范中有两个操作来执行二阶段提交,那么这个协议的具体过程是怎样的呢?是否能够保证分布式事务的一致性?

两阶段提交协议的执行过程,分为投票(Voting)和提交(Commit)两个阶段。

首先,我们看一下第一阶段投票:在这一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不会修改数据库中的数据),待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;

若不成功,则发送“No”消息,表示终止操作。当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令。具体规则如下:

  • 若协调者从参与者那里收到的都是“Yes”消息,则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余的操作(比如修改数据库中的数据)并释放资源(整个事务过程中占用的资源),然后向协调者返回“HaveCommitted”消息;
  • 若协调者从参与者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者,则会根据之前执行操作时的事务日志对操作进行回滚,就好像没有执行过请求操作一样,然后所有参与者会向协调者发送“HaveCommitted”消息;
  • 协调者接收到来自所有参与者的“HaveCommitted”消息后,就意味着整个事务结束了。

二阶段提交的算法思路可以概括为:协调者向参与者下发请求事务操作,参与者接收到请求后,进行相关操作并将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。

虽然基于 XA 的二阶段提交算法尽量保证了数据的强一致性,而且实现成本低,但依然有些不足。主要有以下三个问题:

  • 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。因此,基于 XA 的二阶段提交协议不支持高并发场景。
  • 单点故障问题:该算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待事务管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
  • 数据不一致问题:在提交阶段,当协调者向所有参与者发送“DoCommit”请求时,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。

三阶段提交方法

三阶段提交协议(Three-phase Commit Protocol,3PC),是对二阶段提交(2PC)的改进。为了更好地处理两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段。

  • 与 2PC 只是在协调者引入超时机制不同,3PC 同时在协调者和参与者中引入了超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,从而减少了整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。
  • 在第一阶段和第二阶段中间引入了一个准备阶段,或者说把 2PC 的投票阶段一分为二,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段尽可能排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。

CanCommit 阶段

协调者向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应;参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。

3PC 的 CanCommit 阶段与 2PC 的 Voting 阶段相比:

  • 类似之处在于:协调者均需要向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应。参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。
  • 不同之处在于,在 2PC 中,在投票阶段,若参与者可以执行事务,会将操作信息记录到事务日志中但不提交,并返回结果给协调者。但在 3PC 中,在 CanCommit 阶段,参与者仅会判断是否可以顺利执行事务,并返回结果。而操作信息记录到事务日志但不提交的操作由第二阶段预提交阶段执行。

CanCommit 阶段不同节点之间的事务请求成功和失败的流程,如下所示:

在这里插入图片描述

当协调者接收到所有参与者回复的消息后,进入预提交阶段(PreCommit 阶段)。

PreCommit 阶段

协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作(预提交阶段)。

  • 如果所有参与者回复的都是“Yes”,那么协调者就会执行事务的预执行。
  • 协调者向参与者发送 PreCommit 请求,进入预提交阶段。
  • 参与者接收到 PreCommit 请求后执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。
  • 如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
  • 假如任何一个参与者向协调者发送了“No”消息,或者等待超时之后,协调者都没有收到参与者的响应,就执行中断事务的操作。
  • 协调者向所有参与者发送“Abort”消息。
  • 参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。

预提交阶段,不同节点上事务执行成功和失败的流程,如下所示。

在这里插入图片描述

预提交阶段保证了在最后提交阶段(DoCmmit 阶段)之前所有参与者的状态是一致的。

DoCommit 阶段

DoCmmit 阶段进行真正的事务提交,根据 PreCommit 阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。

  • 执行提交阶段:
    • 若协调者接收到所有参与者发送的 Ack 响应,则向所有参与者发送 DoCommit 消息,开始执行阶段。
    • 参与者接收到 DoCommit 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源,并向协调者发送 Ack 响应。
    • 协调者接收到所有参与者的 Ack 响应之后,完成事务
  • 事务中断阶段:
    • 协调者向所有参与者发送 Abort 请求。
    • 参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作,释放所有锁住的资源,并向协调者发送 Ack 消息。
    • 协调者接收到参与者反馈的 Ack 消息之后,执行事务的中断,并结束事务。

执行阶段不同节点上事务执行成功和失败 (事务中断) 的流程,如下所示。

在这里插入图片描述

3PC 协议在协调者和参与者均引入了超时机制。即当参与者在预提交阶段向协调者发送 Ack 消息后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,从而减少整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。

但三阶段提交仍然存在数据不一致的情况,比如在DoCommit 阶段,协调者向所有参与者发送了Abort事务中断的请求,但是部分参与者与协调者网络不通,导致接收不到消息。此时接收到消息的参与者会回滚事务,未接收到 消息且网络不通的参与者会继续执行任务,最终导致数据不一致。

基于分布式消息的最终一致性方案

PC 和 3PC 核心思想均是以集中式的方式实现分布式事务,这两种方法都存在两个共同的缺点,一是,同步执行,性能差;二是,数据不一致问题。为了解决这两个问题,通过分布式消息来确保事务最终一致性的方案便出现了。

在 eBay 的分布式系统架构中,架构师解决一致性问题的核心思想就是:将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。这个案例,就是使用基于分布式消息的最终一致性方案解决了分布式事务的问题。

基于分布式消息的最终一致性方案的事务处理,引入了一个消息中间件(在本案例中,我们采用 Message Queue,MQ,消息队列),用于在多个应用之间进行消息传递。实际使用中,阿里就是采用 RocketMQ 机制来支持消息事务。

基于消息中间件协商多个节点分布式事务执行操作的示意图,如下所示。

在这里插入图片描述

仍然以网上购物为例。假设用户 A 在某电商平台下了一个订单,需要支付 50 元,发现自己的账户余额共 150 元,就使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。

在该事件中,涉及到了订单系统、支付系统、仓库系统,这三个系统是相互独立的应用,通过远程服务进行调用。

在这里插入图片描述

根据基于分布式消息的最终一致性方案,用户 A 通过终端手机首先在订单系统上操作,通过消息队列完成整个购物流程。然后整个购物的流程如下所示。
在这里插入图片描述

  1. 订单系统把订单消息发给消息中间件,消息状态标记为“待确认”。
  2. 消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为“待发送”的消息。
  3. 消息中间件返回消息持久化结果(成功 / 失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单。
  4. 订单操作完成后,把操作结果(成功 / 失败)发送给消息中间件。
  5. 消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为“待发送(可发送)”,并执行消息投递。
  6. 如果消息状态为“可发送”,则 MQ 会将消息发送给支付系统,表示已经创建好订单,需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。
  7. 支付系统支付完成后,会将支付消息返回给消息中间件,中间件将消息传送给订单系统。若支付失败,则订单操作失败,订单系统回滚到上一个状态,MQ 中相关消息将被删除;若支付成功,则订单系统再调用库存系统,进行出货操作,操作流程与支付系统类似。

在上述过程中,可能会产生如下异常情况,其对应的解决方案为:

  1. 订单消息未成功存储到 MQ 中,则订单系统不执行任何操作,数据保持一致;
  2. MQ 成功将消息发送给支付系统(或仓库系统),但是支付系统(或仓库系统)操作成功的 ACK 消息回传失败(由于通信方面的原因),导致订单系统与支付系统(或仓库系统)数据不一致,此时 MQ 会确认各系统的操作结果,删除相关消息,支付系统(或仓库系统)操作回滚,使得各系统数据保持一致;
  3. MQ 成功将消息发送给支付系统(或仓库系统),但是支付系统(或仓库系统)操作成功的 ACK 消息回传成功,订单系统操作后的最终结果(成功或失败)未能成功发送给 MQ,此时各系统数据可能不一致,MQ 也需确认各系统的操作结果,若数据一致,则更新消息;若不一致,则回滚操作、删除消息。

基于分布式消息的最终一致性方案采用消息传递机制,并使用异步通信的方式,避免了通信阻塞,从而增加系统的吞吐量。同时,这种方案还可以屏蔽不同系统的协议规范,使其可以直接交互。

在不需要请求立即返回结果的场景下, 这些特性就带来了明显的通信优势,并且通过引入消息中间件,实现了消息生成方(如上述的订单系统)本地事务和消息发送的原子性,采用最终一致性的方式,只需保证数据最终一致即可,一定程度上解决了二阶段和三阶段方法要保证强一致性而在某些情况导致的数据不一致问题。

可以看出,分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以,分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。

三种实现方式的对比

下面是关于三种实现方式的对比:

在这里插入图片描述

知识扩展:刚性事务与柔性事务

在讨论事务的时候,我们经常会提到刚性事务与柔性事务,但却很难区分这两种事务。所以,今天的知识扩展内容,我就来和你说说什么是刚性事务、柔性事务,以及两者之间有何区别?

  • 刚性事务,遵循 ACID 原则,具有强一致性。比如,数据库事务。
  • 柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。

总结来讲,与刚性事务不同,柔性事务允许一定时间内,数据不一致,但要求最终一致。而柔性事务的最终一致性,遵循的是 BASE 理论。

那,什么是 BASE 理论呢?eBay 公司的工程师 Dan Pritchett 曾提出了一种分布式存储系统的设计模式——BASE 理论。 BASE 理论包括基本可用(Basically Available)、柔性状态(Soft State)和最终一致性(Eventual Consistency)。

  • 基本可用:分布式系统出现故障的时候,允许损失一部分功能的可用性,保证核心功能可用。比如,某些电商 618 大促的时候,会对一些非核心链路的功能进行降级处理。
  • 柔性状态:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。
  • 最终一致性:事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,所有数据都是一致的。

BASE 理论为了支持大型分布式系统,通过牺牲强一致性,保证最终一致性,来获得高可用性,是对 ACID 原则的弱化。ACID 与 BASE 是对一致性和可用性的权衡所产生的不同结果,但二者都保证了数据的持久性。ACID 选择了强一致性而放弃了系统的可用性。与 ACID 原则不同的是,BASE 理论保证了系统的可用性,允许数据在一段时间内可以不一致,最终达到一致状态即可,也即牺牲了部分的数据一致性,选择了最终一致性。

具体到今天的三种分布式事务实现方式,二阶段提交、三阶段提交方法,遵循的是 ACID 原则,而消息最终一致性方案遵循的就是 BASE 理论。

总结

分布式事务常见的三种实现方式:基于 XA 协议的二阶段提交方法,三阶段方法以及基于分布式消息的最终一致性方法。

二阶段和三阶段方法是维护强一致性的算法,它们针对刚性事务,实现的是事务的 ACID 特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统,它维护的是事务的最终一致性,遵循的是 BASE 理论,因此适用于柔性事务。

最后用一张思维导图来总结一下:

在这里插入图片描述

参考

1、极客时间课程:《分布式技术原理与算法解析》— 06 分布式事务: All or nothing

2、极客时间课程:《分布式协议与算法实战》— MySQL XA是如何实现分布式事务的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值