# 【NO.79】消息队列背后的设计思想

消息队列也通常称为消息中间件，提到消息队列，大部分互联网人或多或少都听过该名词。对于后端工程师而言，更是日常开发中必备的一项技能。随着大数据时代的到来，apache 旗下的 kafka 一度成为消息队列的代名词，提起消息队列大家自然而然就想到了 kafka。近而网上有太多太多介绍消息队列 kafka 功能或者内部实现的文章。

然而消息队列本身是工程领域内一种解决问题的通用方案。它的背后有着一些通用的设计思想和经典模型，这些是消息队列的精髓和灵魂。它们独立于任何一种消息队列的具体实现(例如 kafka)，但每种消息队列(除了 kafka 外，还有 rocketMQ、pulsar 等)的实现中到处体现着这些设计思想。本文主要从抽象层面来简单谈谈消息队列背后的一些设计思想，辅助理解消息队列这一类组件。

本文主要解决三个问题：

1. 消息队列适合什么场景？
2. 消息队列有哪些主流产品、各自的优缺点？
3. 消息队列背后的设计思想(整体核心模型、数据存储考量、数据获取方案对比、消费者消费模型)

### **1.消息队列适合哪些场景？**

**消息队列：它主要用来暂存生产者生产的消息，供后续其他消费者来消费。它的功能主要有两个：a.暂存(存储)、b.队列(有序：先进先出)。其他大部分场景对数据的消费没有顺序要求，主要用它的暂存能力** 。从目前互联网应用中使用消息队列的场景来看，主要有以下三个：
**1. 异步处理数据**
**2. 系统应用解耦**
**3. 业务流量削峰**

下面对上述每一种场景进行简单描述。

#### 1.1 异步处理数据

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151424281393776.png)

第一个例子我们以现实生活中**送快递**来类比，在该例子中我们把**暂存快递的快递柜**比作**暂存数据的消息队列**。我们来看一下在现实生活中，没有快递柜时，快递员把快递送到目的地后，一般需要把联系收货人来签收快递，如果收货人此时有空，那一切都很顺利。但如果收货人此时不方便(开会、正在吃午饭、外出出差)。那对于快递员而言，就很尴尬，需要一直等待(开会 or 吃午饭)或者将快递拿回去(外出出差)，导致白跑一趟。这对于快递员而言简直太不友好。

从这儿可以看出，当快递员送货时，是一个同步状态，即需要等待收货人签收后才能去送下一趟单子，对快递员而言效率太低。上述例子虽然有点牵强，大家凑合理解，意思能大概理解到位就 ok。

接着我们再来看一下，当有了快递柜后，对于快递员而言，每次需要送快递时，只需要将快递投掷到快递柜，然后再通过短信或者电话通知收货人具体的快递信息即可。他就可以继续去派送下一单。而对于收获人而言，也可以根据具体方便的时间来取件。这样一来，二者完全异步了，不用相互等待了。

在这个例子中，如果把快递员比作生产者，收货人比作是消费者，则快递柜就类似于消息队列。我们可以通过采用消息队列来实现异步数据的处理。

#### 1.2 系统应用解耦

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151424396770166.png)

案例二我们以目前最主流的推荐系统中内容的流转来举例。在推荐系统中当创作者发布了一条内容后，该内容会首先经过安全部分的相关审核，通过审核后的内容，通常需要进行内容入库存储、送入模型进行特征的计算和生成。

假如后期我们想提升推荐的效果，需要单独构建一份冷启动的推荐池，此时也需要用到这部分内容，那问题来了，在没有使用消息队列时，对于上游服务而言，需要通过扩展新的逻辑来实现该功能。同时在该场景里，会存在依赖三个下游服务，如果其中一个下游服务失败后，该如何处理，是重试还是返回失败等这些细节的处理。如果后期这部分数据还想在其他渠道分发，那又该如何对接。明显这种场景下面临着系统紧耦合的问题。

我们再来看一下，如果我们一开始就引入了消息队列，那问题又会变成怎样的呢？当内容审核通过后，就直接将数据生产出来丢到消息队列中，下游的多个服务再从消息队列消费数据。当后续这一份数据需要扩展供其他系统使用时，也只要通过新的消费者来接入到消息队列消费就 ok。上游生产消息的模块不要做任何的改动。这样我们就通过消息队列进行了系统应用之间的解耦。这是消息队列的第二个用途。

#### 1.3 业务流量削峰

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151424498148740.png)

消息对应的第三个使用场景便是**削峰**。在现如今的互联网世界中，电商场景中每年的 618 秒杀活动、双 11 抢购便是最典型的案例。这种场景中系统的峰值流量往往集中于一小段时间内，平常的流量比较可控，所以为了防止系统在短时间内的峰值流量冲垮，往往采用消息队列来削弱峰值流量，高峰值期间产生的订单消息等数据首先送入到消息队列中暂存，然后供下游系统根据自己的消费能力来逐步处理。同时这类消息往往对时延的要求不是很高，比较适合采用消息队列暂存。

#### 1.4 小结

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151424596751835.png)

最后我们在对本节的内容做一个简单的总结，上面通过三个简单的实例介绍了消息队列的典型的三个使用场景：**异步**、**解耦**、**削峰**。换个角度来理解可以看到，消息队列主要适用于处理**对消息要求不是很实时，同时一份数据可能会多处使用的场景，不同的使用方处理速率不同。**更多的消息队列的使用场景读者可以自行找资料阅读和总结。

### **2.有哪些消息队列(解决方案)？**

#### 2.1 消息队列主流产品

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151425096063234.png)

上图根据时间线展示了不同时间点产生的消息列队产品，主要的产品有：ActiveMQ(2003)、RabbitMQ(2006)、Kafka(2010)、RocketMQ(2011)、Pulsar(2012)。这些消息队列中或多或少我们都听过一些，部分也在项目中真实使用过。下面对上述几个消息队列做一个简单的介绍。

**ActiveMQ：** ActiveMQ 由 Apache 软件基金会基于 Java 语言开发的一个开源的消息代理。能够支持多个客户机或服务器。计算机集群等属性支持 ActiveMQ 来管理通信系统。

**RabbitMQ：** RabbitMQ 是实现了高级消息队列协议（AMQP）的开源消息代理软件（亦称面向消息的中间件）。RabbitMQ 服务器是用 Erlang 语言编写的，而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。RabbitMQ 支持多种消息传递协议、传递确认等特性。

**Kafka：** Apache Kafka 是由 Apache 软件基金会开发的一个开源消息系统项目，由 Scala 写成。Kafka 最初是由 LinkedIn 开发，并于 2011 年初开源。2012 年 10 月从 Apache Incubator 毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。Kafka 是一个分布式的、分区的、多复本的日志提交服务。它通过一种独一无二的设计提供了一个消息系统的功能。

**RocketMQ：** Apache RocketMQ 是一个分布式消息和流媒体平台，具有低延迟、**强一致**、高性能和可靠性、万亿级容量和灵活的可扩展性。它有借鉴 Kafka 的设计思想，但不是 kafka 的拷贝。

**Pulsar：** Apache Pulsar 是 Apache 软件基金会顶级项目，是下一代云原生分布式消息流平台，集消息、存储、轻量化函数式计算为一体，采用计算与存储分离架构设计，支持多租户、持久化存储、多机房跨区域数据复制，具有**强一致性**、高吞吐、低延时及高可扩展性等流数据存储特性，被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。

#### 2.2 不同消息队列对比

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151425209549929.png)

上图详细的展示了几种消息队列的各自功能及优缺点，首先，ActiveMQ 和 RabbitMQ 二者属于同一量级，在吞吐量上比后面三者差一个量级；其次，支持强一致性的有 RocketMQ 和 Pulsar，在对消息一致性要求比较高的场景可以采用这些产品。同时 kafka 虽然会有数据丢失的风险，但其吞吐量比较高同时社区非常活跃，在大数据的绝大部分场景里，都可以采用 kafka；最后 kafka、RocketMQ、Pulsar 这几款是基于磁盘存储数据的，内存加速访问。而 ActiveMQ、RabbitMQ 采用内存存储数据，也支持数据持久化到磁盘。

### **3. 消息队列背后的设计思想**

在前面，第一节内容中，主要介绍了为什么要使用消息队列，消息队列适合解决哪些问题？在第二节内容中，又介绍了有哪些可选择的消息队列，以及他们之间各自的优缺点。这一节是最重要的内容，主要会介绍一下上述消息队列背后的通用的一些设计思想。部分思想可以扩展到其他的业务模型或者领域内。后面讲到对应内容也会有所提及。

#### 3.1 消息队列核心模型

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151425328494917.png)

上图是几乎所有消息队列设计的一个核心模型。对于一个消息队列而言，从数据流向的维度，可以拆解为三大部分：**生产者**、**消息队列集群**、**消费者**，数据是从生产者流向消息队列集群，最终再从消息队列集群流向消费者，下面对这几个概念进行一一阐述。

**生产者：** 生产数据的服务，通常也称为数据的输入提供方，这里的数据通常指我们的业务数据，例如推荐场景中用户对内容的点击数据、内容曝光数据、电商中的订单数据等等。生产者通常是作为客户端的方式存在，但在支持事务消息的消息队列中，生产者也被设计为服务端，实现事务消息这一特性。其次生产者通常会有多个，消息队列集群内部也会有多个分区队列，所以在生产者发送数据时，通常会存在负载均衡的一些策略，常见的有**按 key hash**、**轮询**、**随机**等方式。其本质是**一条数据，被消息队列封装后也被称为一条消息，该条消息只能发送到其消息队列集群内部的一个分区队列中。因此只需按照一定的策略从多个队列中选择一个队列即可**。

**消息队列集群：** 消息队列集群是消息队列这种组件实现中的核心中的核心，它的主要功能是**存储消息**、**过滤消息**、**分发消息**。

其中存储消息主要指生产者生产的数据需要存储到消息队列内部；存储消息可以说是消息队列的核心，一个消息队列吞吐量的高低、性能优劣都和它的存储模型脱不开关系。这部分内容会在 3.2 节进行介绍。

过滤消息只指消息队列可以通过一定的规则或者策略进行消息的过滤，该项能力通常也被称为消息路由；过滤消息属于高阶的特性功能，AMQP 协议对这些能力抽象的比较完备，部分消息队列可以选择性的实现该协议来达到该功能，关于 AMQP 协议内容读者可以自行搜索资料阅读，此处不再展开。

分发消息是指消息队列通常需要将消息分发给处理同一逻辑的多个消费者处理或者处理不同逻辑的不同消费者处理。分发消息可以说和消费者模型想挂钩，这块会涉及到不同的数据获取方式，也会涉及到消费者消费消息的模型。这两部分内容会在 3.3 节和 3.4 节进行重点介绍。

此外绝大部分的消息队列也都支持对消息进行分类，分类的标签称为**topic(主题)**，一个 topic 中存放的是同一类消息。

**消费者：** 最终消息队列存储的消息会被消费者消费使用，消费者也可以看做消息队列中数据的输出方。消费者通常有两种方式从消息队列中获取数据：**推送(push)数据**、**拉取(pull)数据**，3.3 节中会对这两种方案进行详细对比说明。其次消费者也经常是作为客户端的角色出现在在消息队列这种组件中。

#### 3.2 消息队列数据组织方式

在这一节中，我们详细看看消息队列**存储消息**这个环节的一些权衡考量，通常数据的存储无外乎就是两种，一种是存储在**非易失性存储**中，例如磁盘这种介质；另一种是选择存储在**易失性存储**中，典型的就是内存。关于二者的对比大家可以参考下表，此处就不再赘述。

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151425452332884.png)

通常在大部分组件设计时，往往会选择一种主要介质来存储、另一种介质作为辅助使用。就拿 redis 来说，它主要采用内存存储数据，磁盘用来做辅助的持久化。拿 RabbitMQ 举例，它也是主要采用内存存储消息，但也支持将消息持久化到磁盘。而 RocketMQ、Kafka、Pulsar 这种，则是数据主要存储在磁盘，通过内存来主力提升系统的性能。关系型数据库例如 mysql 这种组件也是主要采用磁盘组织数据，合理利用内存提升性能。

针对采用内存存储数据的方案而言，难点一方面在于如何在不降低访问效率的情况下，充分利用有限的内存空间来存储尽可能多的数据，这个过程中少不了对数据结构的选型、优化；另一方面在于如何保证数据尽可能少的丢失，我们可以看到针对此问题的解决方案通常是快照+广泛意义的 wal 文件来解决。此类典型的代表就是 redis 啦。

针对采用磁盘存储数据的方案而言，难点一方面在于如何根据系统所要解决的特点场景进行合理的对磁盘布局。读多写少情况下采用 b+树方式存储数据；写多读少情况下采用 lsm tree 这类方案处理。另一方面在于如何尽可能减少对磁盘的频繁访问，一些做法是采用 mmap 进行内存映射，提升读性能；还有一些则是采用缓存机制缓存频繁访问的数据。还有一些则是采用巧妙的数据结构布局，充分利用磁盘预读特性保证系统性能。

**总的来说，针对写磁盘的优化，要不采用顺序写提升性能、要不采用异步写磁盘提升性能(异步写磁盘时需要结合 wal 保证数据的持久化，事实上 wal 也主要采用顺序写的特性)；针对读磁盘的优化，一方面是缓存、另一方面是 mmap 内存映射加速读**。

上述这些存储方案上权衡的选择在 kafka、RocketMQ、Pulsar 中都可以看到。其实抛开消息队列而言，这些存储方案的选择上无论是关系型数据库还是 kv 型组件都是通用的。

下图列举了几种磁盘上的数据组织方式，仅供大家参考。

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151425552772473.png)

#### 3.3 获取数据的推、拉两种方案对比

在前面 3.1 节中提到，消费者在从消息队列中获取数据时，主要有两种方案：
**1. 等待推送数据**
**2. 主动拉取数据**

目前的消息队列实现时，都会选择支持两种的至少一种，关于这两种方案的对比，大家可以参考下表。

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151426081211594.png)

在此处，个人想抛开消息队列谈一点关于这两种方案的理解，其实推拉模型不仅仅只用于消息队列这种组件中，更一般意义上，它解决的其实是数据传送双方的一个问题。本质是**数据需要从一方流向另一方**。顺着这个思路来看，下面这三个例子都是遵循这个原则。

**网络中传输的数据：** 在 IO 多路复用中，以 epoll 为例，当内核检测到监听的描述符有数据到来时，epoll_wait()阻塞会返回，上层用户态程序就知道有数据就绪了，然后可以通过 read()系统调用函数读取数据。这个过程就绪通知，类似于推送，但推送的不是数据，而是数据就绪的信号。具体的数据获取方式还是需要通过拉取的方式来主动读。

**feeds 流系统用户时间线后台实现方案(读扩散、写扩散)：** 读扩散和写扩散更是这样一个 case。对于读扩散而言，主要采用拉取的方式获取数据。而对于写扩散而言，它是典型的数据推送的方式。当然在系统实现中，更复杂的场景往往会选择**读写结合**的思路来实现。

**生活中的点外卖例子：** 当下单点外卖时，通常也会有两种方式可以选择，**外卖派送**、**到店自取**。不过通常**外卖派送**比较实时，我们通常就选择这种方式了而已。可以看出外卖派送其实就是一种推的方式，而到店自取，则是拉的方式。

#### 3.4 消息队列消费模型

本节我们来介绍最后一部分内容，消息队列中**消费者的消费模型**。下图中上半部分展示了最简单的一种消费模型。一个生产者、一个消费者。但往往我们的一份数据通常会被不同场景所使用。那这个时候，首先就会存在每种场景需要使用全量的数据、而且不同场景之间不会相关影响，彼此独立。方便理解起见，我们假设有 N 个场景需要使用这同一份数据，每个场景需要消费全量的数据。而在 N 个场景中的一种场景里，又会有多个消费者一起分摊消费这些数据。我们假设一个场景里有 M 个消费者。由于每个场景中包含 M 个消费者，我们也将其采用**消费者组**来描述。通过上面的介绍，我们可以用下面一句话总结消息队列中的消费模型：

**消费者消费者模型其实是一个 1:N:M 的关系，一份数据被 N 个消费者组独立使用，每个消费者组中有 M 个消费者进行分摊消费**

其实这种模型也称为**发布订阅模型**，对于一条消息而言，组间广播、组内单播。一条消息只能被一个消费者组中的一个消费者使用。在消费者组内部也存在一些负载均衡的策略。常用的有：**轮询**、**随机**、**hash**、**一致性 hash**等方案。

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/202212151426413270920.png)

#### 3.5 小结

第三部分内容我们重点介绍了关于消息队列背后的一些设计思想，其中包括：**消息队列的核心模型**、**数据存储模型**、**推拉方案获取数据对比**、**消费者消费模型**。其中数据存储模型不仅仅适用于消息队列，也同样适用于其他数据存储组件的方案选择。同理数据获取的推拉两种方案也不仅仅局限于消息队列。我们可以在很多业务场景里看到同类思想的影子。

### **4. 总结**

到此，本文也就告一段落了。本文主要从理论、抽象层面泛泛的谈了下关于消息队列的一些思想和理念。主要介绍了消息队列的使用场景，主流的消息队列可选方案以及他们之间的优缺点。最后介绍了一些关于消息队列背后的设计理念。本文只是抛砖引玉，希望上述内容能辅助大家一起重新认识消息队列。后面会逐步挑选上述的几种消息队列(kafka、RocketMQ、Pulsar)，重点分析其内部实现机制，敬请期待。限于个人水平有限，理解有误之处欢迎大家批评指正。

### **5. 参考资料**

1. [ActiveMQ 与 RabbitMQ 的区别](https://javakk.com/1721.html)
2. [Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理](https://cloud.tencent.com/developer/article/1408126)
3. [Kafka、RabbitMQ、RocketMQ 等消息中间件的对比](https://blog.csdn.net/belvine/article/details/80842240)
4. [Apache Pulsar 在腾讯计费场景下的应用](https://mp.weixin.qq.com/s?__biz=MzUxOTc4NDc2MQ==&mid=2247485123&idx=1&sn=d79f5f085646d75280455753ea7bc6fa&source=41&scene=21#wechat_redirect)
5. [kafka Push vs. pull](https://kafka.apache.org/documentation/#design_pull)
6. [消息队列-推/拉模式学习 & ActiveMQ 及 JMS 学习](https://blog.csdn.net/u014469692/article/details/79866330?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-9&spm=1001.2101.3001.4242)

原文作者：jaydenwen，腾讯 PCG 后台开发工程师

原文链接：https://mp.weixin.qq.com/s/k8sA6XPrp80JiNbuwKaVfg