利用MQ的分布式事务实现思想

本篇我们来看下另外一类分布式事务的实现思路,即利用 MQ 实现的分布式事务。通过前两篇 2PCTCC 打下的基础,基于 MQ 的分布式事务会更容易理解。

消息队列在架构设计中的角色,起到的作用主要是 解耦,异步。应用场景有很多,所以消息队列也是最关键的中间件之一。市面上才会出现这么多种的消息队列,kafka也成了必备知识点。

假设成功发送给 MQ 的消息不会丢失,这样,MQ 下游的服务一定会在某一时刻将消息消费掉。

以上这句话,引出了三个问题:

  1. 上游服务发送给 MQ 的消息怎么算成功?
  2. MQ 成功收到消息后,消息是否会丢失,如何保证不丢失?
  3. MQ如何保证让下游服务成功消费消息?

这三个问题,是理解基于 MQ 的分布式事务机制的关键。我们先抛开分布式事务,只关注消息本身,来分析上述的三个问题。

问题1

上游服务给 MQ 发送消息后,由于网络等原因(这点在前面分布式系统相关文章中有说明),其实是不知道 MQ 是否成功收到消息的。

那么,该如何实现呢?答案就是应答机制。当消息生产者向消息队列发送一条消息时,正常流程中,消息队列会回复ack给生产者。这样生产者就可以知道消息已经发送成功。

问题2

问题2其实核心思想就很简单了,因为前文我们在 mysql指引(十一):innodb基本结构和执行逻辑拆解 中已经学习过 innodb 是如何保证数据不丢失的。

即同步刷盘机制,并且由于存在 WAL 机制,使数据库可以兼顾数据可靠性和性能。
也就是说,MQ 会在将消息持久化到磁盘后,才给上游服务响应ack。

问题3

这个问题实际上等同于问题1,需要下游增加一个消费确认ack。如果因为网络异常等原因,导致 MQ 一直没有收到下游的ack,则 MQ 会重新发送消息。

注意,重发前,下游是否已经消费过消息,MQ 是无从得知的。所以下游接口需要做 幂等性 设计。


当 MQ 成功收到消息后,剩下的就是 MQ 和下游服务的事情了。而 MQ 只要保证消息能被下游服务消费就可以了,至于是立即消费掉还是等一会才能消费掉,并不需要关心。

所以实际使用 MQ 的情境下,整体系统是呈现最终一致性的。并且,当下游未能消费成功时,是无法让上游执行回滚的,所以只能不断重试,直到最终消费成功。


现在,让我们回到分布式事务上来。首先面临的一个问题是:

上游服务的本地数据库操作,如何与 写 MQ 消息保持原子性?

理想情况下,上游服务成功执行本地数据库操作后,也会成功写入 MQ。

利用MQ的分布式事务实现思想

但是,假如上游服务未收到ack,则可能是 MQ 没有收到消息,也可能是 MQ 收到了消息,但是返回 ack 的时候出了问题。

不论上游服务的本地数据库进行回滚或者提交,都无法保证和 MQ 的消息是一致的。万一上游回滚了,但是 MQ 已经持久化了,那么这两端就成了不一致。

那么应该怎么办呢?通过前面的学习,答案自然是借鉴 2PC 的思想,所以为什么2PC看起来简单,但是非常经典。我们既可以让上游服务成为两阶段的主角,也可以让 MQ 成为两阶段的主角。


先来看上游服务为主的两阶段提交方案,也即利用本地消息实现的分布式事务。

利用MQ的分布式事务实现思想

上游服务先写数据库,成功后将消息也写到数据库中,此时消息为待处理状态。其实只要写到了本地数据库中,上游服务就可以返回给客户端本次请求的结果了。

然后上游服务可以不断扫描本地数据库中处于待处理状态的消息,执行写 MQ,假如 MQ 写入成功(上游收到 ack),那么就可以将本消息标记为已处理,或者直接删除该记录。

另外一种以 MQ 为主的两阶段提交方案,需要 MQ 支持 事务消息。就和数据库一样,有的数据库就不支持事务。

利用MQ的分布式事务实现思想

这种方案中,上游服务需要先向 MQ 发送消息,但是消息处于待处理状态。发送成功后,上游服务再执行业务逻辑(即写入数据库)。写入成功,则再次请求 MQ 执行提交,通知将消息状态变为可消费状态。

当出现异常时,也就是待处理消息已经写入 MQ 了,但是 MQ 一直未接收到 提交请求。同时,上游服务也无法知晓 MQ 是否已经执行提交。这种情况下,主要是看 MQ 的实现策略。比如超时时间到了,可以主动向上游反查,这样就需要上游服务额外提供反查接口。


对于 MQ 和下游服务来说,整体过程就很简单了。只要下游保证幂等性,那么 MQ 就可以在异常时不断重发消息,直到消息被消费掉。


到这里,我们已经知道常见的 MQ 分布式事务。在讲述过程中,见到了多次不一致,不论是事务的原子性、持久性、隔离性哪个,实际上也都是在为一致性做保证。

下篇文章,就针对分布式系统的各种一致性做分析,比如线性一致性、因果一致性等。再之后,就可以进入分布式一致性算法的世界。