引言
对于我们网上购物的每一笔订单来说,平台都会有两个核心步骤:一是订单业务采取下订单操作,二是库存业务采取减库存操作。
通常,这种不同业务会运行在不同的机器上边的,假设这两个动作是在同一台机器上发生的,我们进行操作的时候是不是需要保证订单操作和库存操作动作一致才能保证这个交易的准确性(通常我们用mysql事务来保证),如果这个问题放到了分布式结构中,我们是不是同样需要保证操作的正确性,那么这个问题,就是分布式事务。
什么是分布式事务
首先,事务大家都不陌生,事务就是包含一系列操作的、一个有边界的工作序列,有明确的开始和结束标志,且要 么被完全执行,要么完全失败,这种叫做本地事务或者单机事务。
那什么是分布式事务呢?顾名思义,在分布式系统中运行的事务叫分布式事务,它其实是由多个本地事务组合而成。因为分布式事务是由事务组成而成的产物,那么分布式事务固然能够基本满足ACID,只不过随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了 BASE 理论,该理论的一个关键点就是采用最终一致性代替强一致性。
如何实现分布式事务
实际上,分布式事务主要是解决在分布式环境下,组合事务的一致性问题。实现分布式事务 有以下 3 种基本方法:
- 基于 XA 协议的二阶段提交协议方法;
- 三阶段提交协议方法;
- 基于消息的最终一致性方法。
其中,基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法,采用了强一致性,遵从 ACID,基于消息的最终一致性方法,采用了最终一致性,遵从 BASE 理论。
接下来,我们就来看看这三种方法到底是何方神圣。
二阶段提交协议方法
二阶段提交协议(下文统称为2PC)有两个阶段:Prepare(投票)和Commit(提交)。在无failure情况下的2PC协议流程的画风是这样的,首先看阶段1:
从图中可以看出,有几个步骤:
- 询问 协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。
- 执行 各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将 Undo 和 Redo 信息记录事务日志中。
- 响应 如果参与者成功执行了事务并写入 Undo 和 Redo 信息,则向协调者返回 YES 响应,否则返回 NO 响应。当然,参与者也可能宕机,从而不会返回响应。
- 阶段1中参与者因为宕机或者网络问题等造成协调者无法接收到所有参与者YES的回应或者某一个节点返回了NO回应,协调者会发送回退命令,道理和我们在使用mysql中rollback一致。
这里需要注意的是,2PC阶段1是执行了事务并写入了Undo和Redo信息,但是没有提交,只是给协调者返回了YES的信号。当第一阶段中协调者收到的ack都为YES的时候进入第二阶段-提交。首先,我们看下流程图:
他又有什么步骤呢?请看下文:
- 首先协调者向所有的参与者发送commit请求。
- 参与者执行commit动作,注意这里只是执行提交,提交完成后释放资源。
- 将执行结果返回给协调者。
- 协调者接收到所有参与者的响应后作出提交事务动作。
当然,上边两个阶段我们都是在各个参与者都成功的情况下,也有可能会有失败的情况,我们看下边几个case:
- 当我们一阶段执行的时候,每一个参与者都在等待协凋者的响应,试想一下,如果其中一个参与者宕机了,那么协凋者就会一直等待下去,从而导致所有的参与者都在等待协调者的下一步指令。当然,2PC存在超时事务中断的处理(协调者的功能),但在这个超时时间段内,依然避免不了所有参与者的阻塞。
- 在2PC中,协调者拥有绝对的权利(当然,这是在提交命令发出之前),如果协调者挂了,那么就会使参与者一直阻塞并一直占用资源,当然,我们可以用主备来保证服务可用性,但是新的协调者它是无法获取上一个协调者的状态信息,所以就无法处理上一个的事务,同样会引起遗留的事务的阻塞。
- 在第二阶段中,在发送出提交指令之后的一切事情都不受协调者控制了,因为事务已经提交了,协调者能做的也就是等待超时后像事务返回一个“我不确定该事务是否执行成功”的命令,然而它确于事无补。当然,还会有在提交的时候因为协调者宕机或者网络问题导致部分参与者没有接收到协调者指令,这就导致各参与者不一致的问题。
很无奈,2PC存在这么多的问题,不过不用怕,已经有人帮我们解决了,针对于这么多的缺点,诞生了3PC(三阶段提交协议)。下边让我们看看3PC是什么妖魔鬼怪。
三阶段提交协议方法
三阶段提交协议(Three-phase commit protocol,3PC),顾名思义,它有三个阶段:CanCommit,PreCommit和DoCommit。接下来让我们一起来看一看它们的真面貌。
cancommit阶段流程图如下:
第一眼看到这个图,有没有发现和2PC的第一阶段很像,是的,确实是。但我们接着看它的执行流程,看看到底哪里不一样。
- 协调者向所有的参与者发送CanCommit请求,请求中包含事务的内容,询问是否可以执行事务提交操作,并开始等待参与者响应。
- 参与者收到事务内容之后进行分析,判断自身是否可以执行,如果可以就返回YSE,进入预备状态,否则返回NO。当收到一个NO或者以上的回应时会产生事务中断。
注意:这个阶段是协调者发送事务内容给参与者,参与者本身并没有执行事务的,而在2PC的第一阶段是协调者发送事务请求之后各个参与者执行了请求之后返回状态。
当所有的参与者经过自己一波猛如虎的分析操作之后,返回自己都OK的情况下进入第二阶段--PreCommit。
- 协调者发送PreCommit请求给参与者。这时候就是进入到一个奇妙的时刻(这个阶段就是2PC的第一阶段)。
- 参与者收到请求之后,执行事务操作,并将 Undo 和 Redo 信息记录事务日志中。
- 如果执行成功进行反馈,等待下一步操作。
虽然这个阶段的动作类似于2PC的阶段1,但是还是有点不一样的地方的,我们继续看?我们看到途中增加了一个新名词Abort,这个是干什么的呢?往下看:
- 当第一阶段中协调者收到NO或者超时等情况下,协调者向参与者发送中断事务请求,其实这个2PC也有,但是为什么会这次会在图上表现出来了呢?因为我们要提出一个新的东西,请往下看。
- 参与者收到 Abort 请求后,会触发事务中断。此外,如果参与者在等待协调者指令超时,会自己触发事务中断,在 2PC 中,参与者会一直阻塞的等待协调者指令,所以 3PC 中解决了因为这种情况带来的阻塞。
协调者根据第二阶段的响应决定最终操作,如果协调者收到了所有参与者在 PreCommit 阶段的 Ack 响应,那么会进入执行事务提交阶段,否则执行事务中断。来梳理下流程图:
- 协调者收到所有参与者在 PreCommit 阶段返回的 Ack 响应后,向所有参与者发送 doCommit 请求,并进入提交状态。
- 参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
- 参与者完成事务提交之后,向协调者返回 Ack 响应。
- 协调者收到所有参与者的 Ack 响应后,完成事务。
这个流程其实和2PC的第二阶段一样,没什么好说的了,需要提及一点的是,如果参与者在等待DoCommir超时的时候,参与者自己会提交,减少阻塞。这样即使协调者挂了也能保证顺利向下走。
对比一下2PC和3PC,有什么区别呢?3PC比2PC多了一个CanCommit的步骤,在提交前先问参与者能不能提交,而不是直接发送执行命令,好像更人性化了一点呢。我们之前说3PC是为了解决2PC的问题,那么它究竟是解决了哪些问题呢?我们来捋一捋。
- 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时,则自动 abort,降低了阻塞;参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞;总结下就是自动提交解决了阻塞。
- 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若协调者宕机,等待超时后自动 abort;参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若协调者宕机,等待超时后自动 commit 事务;总结下就是自动提交解决单点阻塞。
但是2PC还有一个问题没有解决你有没有发现,那就是数据不一致的问题,比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,数据就不一致了。当然如果你说在CanCommit请求响应后参与者没有接到PreCommit的请求,这个其实不会导致数据不一致么因为如果协调者接受不到成功结果他就会进行事务中断的。
其实2PC和3PC还有一个共同的缺点,那就是他们在整个流程中都会锁住资源,导致系统性能降低。那么针对以上两个缺点,该怎么办呢?这时候基于分布式消息的最终一致性方案解决了这个问题,我们看一看这位老大哥是怎么做到的。
基于分布式消息的最终一致性方案
基于分布式消息的最终一致性方案的事务处理,引入了一个消息中间件(Message Queue,MQ),用于在多个应用之间进行消息传递。基于消息中间件协商多个节点分布式 事务执行操作的示意图,如下所示。
单纯的看定义不太容易理解,我们来举个例子:以阿里巴巴的 RocketMQ 中间件为例,分析下其设计和实现思路。
RocketMQ 第一阶段发送 Prepared 消息时,会拿到消息集群返回的消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的消息地址去访问消息,并修改状态。聪明的你可能会问了,如果确认消息发送失败了怎么办?
RocketMQ 会定期扫描消息集群中的事物消息,这时候发现了 Prepared 消息,它会向消息发送者确认,Bob 的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:
那么问题又来了,怎么确保消息接收者收到了消息呢?毕竟消费者消费也可能会出现问题。首先,主流的 MQ 产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些 MQ 可以自定义重试次数)。当然,如果我们消费成功了然后在执行接收者事务的操作时候失败了,我们应该有详细日志记录以及邮件报警,通知相关人员去处理。
那么从上文例子我们可以看出,分布式事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,数据都是一致的。
下期预告
【分布式系统遨游】分布式资源调度
关注我们
欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。