细品分布式事务

2021-02-04 10:30:03 浏览数 (1)

背景

我们平时的单机事物的使用,一步操作,要么全部执行完成,要么全部不执行,也就是ALL or Nothing。但是如果我们使用了分布式,一件事情分为多个分别在多个在不同的机器(进程)上执行。那对于这种的事物我们应该如何控制呢?

事物

ACID属性

  • Atomicity :原子性,原子是人类目前发现最小单元,他是不能再分的,也就是事物也具有这个属性。
  • Consistency:一致性,事物在执行前后,具有完整性约束。也就是保证该事物的各个步骤下来是是正确的
  • Isolation:隔离性,也许你立马想到了mysql的4种隔离级别,读提交,读未提交,可重复读取,串行化。这里的隔离的意思是:多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
  • Durability:持久化,当事物执行完成后,会持久化下来永久保存。 了解了事物的4大特性,那我们接下来了解一下分布式事物

分布式事物

简介

  • 既然说到分布式,那肯定是多台机器的上的。那就是多个机器(进程)事物组成一个分布式事物。其中的任何一个事物失败可以做到整个事物的回滚。并且符合事物的4大特性。
  • 但是由于分布式系统在快速应用,导致其ACID中的C强一致性不能完全做到,就是是技术能力跟不上应用需求,于是大佬们也就提出了base理论。也就是我们所说的最终一致性。
  • 那base理论我们再品一下,BASE理论是Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写。其中的软状态我个人觉得比较难理解:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。 (BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,才用适当的方式来使系统打到最终一致性。来源:掘金

分布式事物的实现方式

基于 XA 协议的二阶段提交协议
  • wiki:对于XA协议的理解:在计算技术上,XA规范是开放群组关于分布式事务处理 (DTP)的规范。规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列,等等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。XA使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。
  • 如果让我们自己实现一个分布式事物:首先我们得达到一处异常全部回滚的目标,那么各个本地事物记得得知其他事物的运行状态,那就得搞一个协调着,也就是来当其中任何一个有问题了,就可以及时告知其中的一个,然后可以回滚自己的操作。我们来看看大佬们是如何设计的
执行过程
  • 两阶段提交协议的执行过程,分为投票(Voting)和提交(Commit)两个阶段。
1.投票

首先,我们看一下第一阶段投票:在这一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不会修改数据库中的数据),待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。

2. 提交阶段
  • 在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令。
  • 具体规则如下:若协调者从参与者那里收到的都是“Yes”消息,则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余的操作(比如修改数据库中的数据)并释放资源(整个事务过程中占用的资源),然后向协调者返回“HaveCommitted”消息;
  • 若协调者从参与者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者,则会根据之前执行操作时的事务日志对操作进行回滚,就好像没有执行过请求操作一样,然后所有参与者会向协调者发送“HaveCommitted”消息;协调者接收到来自所有参与者的“HaveCommitted”消息后,就意味着整个事务结束了。
3.缺点
  • 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。因此,基于 XA 的二阶段提交协议不支持高并发场景。
  • 单点故障问题:该算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
  • 数据不一致问题:在提交阶段,当协调者向所有参与者发送“DoCommit”请求时,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
三阶段提交

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

  • 三阶段提交协议(Three-phase Commit Protocol,3PC),是对二阶段提交(2PC)的改进。为了更好地处理两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段。
  • 与 2PC 只是在协调者引入超时机制不同,3PC 同时在协调者和参与者中引入了超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,从而减少了整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。
  • 在第一阶段和第二阶段中间引入了一个准备阶段,或者说把 2PC 的投票阶段一分为二,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段尽可能排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。三阶段提交协议就有 CanCommit、PreCommit、DoCommit 三个阶段,下面我们来看一下这个三个阶段。
第一阶段
  • 第一,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”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。
  • 预提交阶段,不同节点上事务执行成功和失败的流程,如下所示。
第三,DoCommit 阶段。
  • DoCmmit 阶段进行真正的事务提交,根据 PreCommit 阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。
  • 执行提交阶段:若协调者接收到所有参与者发送的 Ack 响应,则向所有参与者发送 DoCommit 消息,开始执行阶段。
  • 参与者接收到 DoCommit 消息之后,正式提交事务。
  • 完成事务提交之后,释放所有锁住的资源,并向协调者发送 Ack 响应。
  • 协调者接收到所有参与者的 Ack 响应之后,完成事务。
  • 事务中断阶段:协调者向所有参与者发送 Abort 请求。
  • 参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作,释放所有锁住的资源,并向协调者发送 Ack 消息。
  • 协调者接收到参与者反馈的 Ack 消息之后,执行事务的中断,并结束事务。
  • 执行阶段不同节点上事务执行成功和失败 (事务中断) 的流程,如下所示。

3PC 协议在协调者和参与者均引入了超时机制。即当参与者在预提交阶段向协调者发送 Ack 消息后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,从而减少整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。但三阶段提交仍然存在数据不一致的情况,比如在 PreCommit 阶段,部分参与者已经接受到 ACK 消息进入执行阶段,但部分参与者与协调者网络不通,导致接收不到 ACK 消息,此时接收到 ACK 消息的参与者会执行任务,未接收到 ACK 消息且网络不通的参与者无法执行任务,最终导致数据不一致。

基于消息的最终一致性方法

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

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

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

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

仍然以网上购物为例。假设用户 A 在某电商平台下了一个订单,需要支付 50 元,发现自己的账户余额共 150 元,就使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。在该事件中,涉及到了订单系统、支付系统、仓库系统,这三个系统是相互独立的应用,通过远程服务进行调用。

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

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

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

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

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

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

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

三种实现方式对比现在,为了方便你理解并记忆这三种方法,我总结了一张表格,从算法一致性、执行方式、性能等角度进行了对比:

总结

  • 从事务的 ACID 特性出发,介绍了分布式事务的概念、特征,以及如何实现分布式事务。在关于如何实现分布式的部分,我以网购为例,与你介绍了常见的三种实现方式,即基于 XA 协议的二阶段提交方法,三阶段方法以及基于分布式消息的最终一致性方法。
  • 二阶段和三阶段方法是维护强一致性的算法,它们针对刚性事务,实现的是事务的 ACID 特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统,它维护的是事务的最终一致性,遵循的是 BASE 理论,因此适用于柔性事务。
  • 在分布式系统的设计与实现中,分布式事务是不可或缺的一部分。可以说,没有实现分布式事务的分布式系统,不是一个完整的分布式系统。分布式事务的实现过程看似复杂,但将方法分解剖析后,你就会发现分布式事务的实现是有章可循的。

声明:本文大部分摘自:https://time.geekbang.org/column/intro/233

0 人点赞