mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
[docs delete]移除消息队列的一些文章
This commit is contained in:
parent
991609379c
commit
ccf963b5eb
@ -384,9 +384,9 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
|
|||||||
|
|
||||||
消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: [消息队列常见问题总结](./docs/high-performance/message-queue/message-queue.md)。
|
消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: [消息队列常见问题总结](./docs/high-performance/message-queue/message-queue.md)。
|
||||||
|
|
||||||
- **RabbitMQ** : [RabbitMQ 基础知识总结](./docs/high-performance/message-queue/rabbitmq-intro.md)、[RabbitMQ 常见面试题](./docs/high-performance/message-queue/rabbitmq-questions.md)
|
- [RabbitMQ 常见面试题](./docs/high-performance/message-queue/rabbitmq-questions.md)
|
||||||
- **RocketMQ** : [RocketMQ 基础知识总结](./docs/high-performance/message-queue/rocketmq-intro.md)、[RocketMQ 常见面试题总结](./docs/high-performance/message-queue/rocketmq-questions.md)
|
- [RocketMQ 常见面试题总结](./docs/high-performance/message-queue/rocketmq-questions.md)
|
||||||
- **Kafka** :[Kafka 常见问题总结](./docs/high-performance/message-queue/kafka-questions-01.md)
|
- [Kafka 常见问题总结](./docs/high-performance/message-queue/kafka-questions-01.md)
|
||||||
|
|
||||||
## 高可用
|
## 高可用
|
||||||
|
|
||||||
|
@ -504,9 +504,7 @@ export default sidebar({
|
|||||||
children: [
|
children: [
|
||||||
"message-queue",
|
"message-queue",
|
||||||
"kafka-questions-01",
|
"kafka-questions-01",
|
||||||
"rocketmq-intro",
|
|
||||||
"rocketmq-questions",
|
"rocketmq-questions",
|
||||||
"rabbitmq-intro",
|
|
||||||
"rabbitmq-questions",
|
"rabbitmq-questions",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1,312 +0,0 @@
|
|||||||
---
|
|
||||||
title: RabbitMQ基础知识总结
|
|
||||||
category: 高性能
|
|
||||||
tag:
|
|
||||||
- RabbitMQ
|
|
||||||
- 消息队列
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一 RabbitMQ 介绍
|
|
||||||
|
|
||||||
这部分参考了 《RabbitMQ 实战指南》这本书的第 1 章和第 2 章。
|
|
||||||
|
|
||||||
### 1.1 RabbitMQ 简介
|
|
||||||
|
|
||||||
RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。
|
|
||||||
|
|
||||||
RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性、扩展性、可靠性和高可用性等方面的卓著表现是分不开的。RabbitMQ 的具体特点可以概括为以下几点:
|
|
||||||
|
|
||||||
- **可靠性:** RabbitMQ 使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。
|
|
||||||
- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们讲 RabbitMQ 核心概念的时候详细介绍到。
|
|
||||||
- **扩展性:** 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
|
|
||||||
- **高可用性:** 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
|
|
||||||
- **支持多种协议:** RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。
|
|
||||||
- **多语言客户端:** RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。
|
|
||||||
- **易用的管理界面:** RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。
|
|
||||||
- **插件机制:** RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制。
|
|
||||||
|
|
||||||
### 1.2 RabbitMQ 核心概念
|
|
||||||
|
|
||||||
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。
|
|
||||||
|
|
||||||
下面再来看看图 1—— RabbitMQ 的整体模型架构。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
下面我会一一介绍上图中的一些概念。
|
|
||||||
|
|
||||||
#### 1.2.1 Producer(生产者) 和 Consumer(消费者)
|
|
||||||
|
|
||||||
- **Producer(生产者)** :生产消息的一方(邮件投递者)
|
|
||||||
- **Consumer(消费者)** :消费消息的一方(邮件收件人)
|
|
||||||
|
|
||||||
消息一般由 2 部分组成:**消息头**(或者说是标签 Label)和 **消息体**。消息体也可以称为 payLoad ,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。
|
|
||||||
|
|
||||||
#### 1.2.2 Exchange(交换器)
|
|
||||||
|
|
||||||
在 RabbitMQ 中,消息并不是直接被投递到 **Queue(消息队列)** 中的,中间还必须经过 **Exchange(交换器)** 这一层,**Exchange(交换器)** 会把我们的消息分配到对应的 **Queue(消息队列)** 中。
|
|
||||||
|
|
||||||
**Exchange(交换器)** 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 **Producer(生产者)** ,或许会被直接丢弃掉 。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
|
|
||||||
|
|
||||||
**RabbitMQ 的 Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略**:**direct(默认)**,**fanout**, **topic**, 和 **headers**,不同类型的 Exchange 转发消息的策略有所区别。这个会在介绍 **Exchange Types(交换器类型)** 的时候介绍到。
|
|
||||||
|
|
||||||
Exchange(交换器) 示意图如下:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
生产者将消息发给交换器的时候,一般会指定一个 **RoutingKey(路由键)**,用来指定这个消息的路由规则,而这个 **RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效**。
|
|
||||||
|
|
||||||
RabbitMQ 中通过 **Binding(绑定)** 将 **Exchange(交换器)** 与 **Queue(消息队列)** 关联起来,在绑定的时候一般会指定一个 **BindingKey(绑定建)** ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
|
|
||||||
|
|
||||||
Binding(绑定) 示意图:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
|
|
||||||
|
|
||||||
#### 1.2.3 Queue(消息队列)
|
|
||||||
|
|
||||||
**Queue(消息队列)** 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
|
|
||||||
|
|
||||||
**RabbitMQ** 中消息只能存储在 **队列** 中,这一点和 **Kafka** 这种消息中间件相反。Kafka 将消息存储在 **topic(主题)** 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。 RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
|
|
||||||
|
|
||||||
**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
|
|
||||||
|
|
||||||
**RabbitMQ** 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
|
|
||||||
|
|
||||||
#### 1.2.4 Broker(消息中间件的服务节点)
|
|
||||||
|
|
||||||
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
|
|
||||||
|
|
||||||
下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整个流程。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这样图 1 中的一些关于 RabbitMQ 的基本概念我们就介绍完毕了,下面再来介绍一下 **Exchange Types(交换器类型)** 。
|
|
||||||
|
|
||||||
#### 1.2.5 Exchange Types(交换器类型)
|
|
||||||
|
|
||||||
RabbitMQ 常用的 Exchange Type 有 **fanout**、**direct**、**topic**、**headers** 这四种(AMQP 规范里还提到两种 Exchange Type,分别为 system 与 自定义,这里不予以描述)。
|
|
||||||
|
|
||||||
##### ① fanout
|
|
||||||
|
|
||||||
fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
|
|
||||||
|
|
||||||
##### ② direct
|
|
||||||
|
|
||||||
direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
以上图为例,如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
|
|
||||||
|
|
||||||
direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。
|
|
||||||
|
|
||||||
##### ③ topic
|
|
||||||
|
|
||||||
前面讲到 direct 类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:
|
|
||||||
|
|
||||||
- RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
|
|
||||||
- BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串;
|
|
||||||
- BindingKey 中可以存在两种特殊字符串“\*”和“#”,用于做模糊匹配,其中“\*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
以上图为例:
|
|
||||||
|
|
||||||
- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2;
|
|
||||||
- 路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中;
|
|
||||||
- 路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中;
|
|
||||||
- 路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中;
|
|
||||||
- 路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。
|
|
||||||
|
|
||||||
##### ④ headers(不推荐)
|
|
||||||
|
|
||||||
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
|
|
||||||
|
|
||||||
## 二 安装 RabbitMQ
|
|
||||||
|
|
||||||
通过 Docker 安装非常方便,只需要几条命令就好了,我这里是只说一下常规安装方法。
|
|
||||||
|
|
||||||
前面提到了 RabbitMQ 是由 Erlang 语言编写的,也正因如此,在安装 RabbitMQ 之前需要安装 Erlang。
|
|
||||||
|
|
||||||
注意:在安装 RabbitMQ 的时候需要注意 RabbitMQ 和 Erlang 的版本关系,如果不注意的话会导致出错,两者对应关系如下:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 2.1 安装 erlang
|
|
||||||
|
|
||||||
**1 下载 erlang 安装包**
|
|
||||||
|
|
||||||
在官网下载然后上传到 Linux 上或者直接使用下面的命令下载对应的版本。
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb local]#wget https://erlang.org/download/otp_src_19.3.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
erlang 官网下载:[https://www.erlang.org/downloads](https://www.erlang.org/downloads)
|
|
||||||
|
|
||||||
**2 解压 erlang 安装包**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb local]#tar -xvzf otp_src_19.3.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
**3 删除 erlang 安装包**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb local]#rm -rf otp_src_19.3.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
**4 安装 erlang 的依赖工具**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb local]#yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel unixODBC-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
**5 进入 erlang 安装包解压文件对 erlang 进行安装环境的配置**
|
|
||||||
|
|
||||||
新建一个文件夹
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb local]# mkdir erlang
|
|
||||||
```
|
|
||||||
|
|
||||||
对 erlang 进行安装环境的配置
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb otp_src_19.3]#
|
|
||||||
./configure --prefix=/usr/local/erlang --without-javac
|
|
||||||
```
|
|
||||||
|
|
||||||
**6 编译安装**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb otp_src_19.3]#
|
|
||||||
make && make install
|
|
||||||
```
|
|
||||||
|
|
||||||
**7 验证一下 erlang 是否安装成功了**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb otp_src_19.3]# ./bin/erl
|
|
||||||
```
|
|
||||||
|
|
||||||
运行下面的语句输出“hello world”
|
|
||||||
|
|
||||||
```erlang
|
|
||||||
io:format("hello world~n", []).
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
大功告成,我们的 erlang 已经安装完成。
|
|
||||||
|
|
||||||
**8 配置 erlang 环境变量**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb etc]# vim profile
|
|
||||||
```
|
|
||||||
|
|
||||||
追加下列环境变量到文件末尾
|
|
||||||
|
|
||||||
```shell
|
|
||||||
#erlang
|
|
||||||
ERL_HOME=/usr/local/erlang
|
|
||||||
PATH=$ERL_HOME/bin:$PATH
|
|
||||||
export ERL_HOME PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
运行下列命令使配置文件`profile`生效
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb etc]# source /etc/profile
|
|
||||||
```
|
|
||||||
|
|
||||||
输入 erl 查看 erlang 环境变量是否配置正确
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb etc]# erl
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 2.2 安装 RabbitMQ
|
|
||||||
|
|
||||||
**1. 下载 rpm**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wget https://www.rabbitmq.com/releases/rabbitmq-server/v3.6.8/rabbitmq-server-3.6.8-1.el7.noarch.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
或者直接在官网下载
|
|
||||||
|
|
||||||
[https://www.rabbitmq.com/install-rpm.html](https://www.rabbitmq.com/install-rpm.html)
|
|
||||||
|
|
||||||
**2. 安装 rpm**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
|
|
||||||
```
|
|
||||||
|
|
||||||
紧接着执行:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yum install rabbitmq-server-3.6.8-1.el7.noarch.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
中途需要你输入"y"才能继续安装。
|
|
||||||
|
|
||||||
**3 开启 web 管理插件**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
rabbitmq-plugins enable rabbitmq_management
|
|
||||||
```
|
|
||||||
|
|
||||||
**4 设置开机启动**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
chkconfig rabbitmq-server on
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. 启动服务**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
service rabbitmq-server start
|
|
||||||
```
|
|
||||||
|
|
||||||
**6. 查看服务状态**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
service rabbitmq-server status
|
|
||||||
```
|
|
||||||
|
|
||||||
**7. 访问 RabbitMQ 控制台**
|
|
||||||
|
|
||||||
浏览器访问:http://你的 ip 地址:15672/
|
|
||||||
|
|
||||||
默认用户名和密码:guest/guest; 但是需要注意的是:guest 用户只是被容许从 localhost 访问。官网文档描述如下:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
“guest” user can only connect via localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决远程访问 RabbitMQ 远程访问密码错误**
|
|
||||||
|
|
||||||
新建用户并授权
|
|
||||||
|
|
||||||
```shell
|
|
||||||
[root@SnailClimb rabbitmq]# rabbitmqctl add_user root root
|
|
||||||
Creating user "root" ...
|
|
||||||
[root@SnailClimb rabbitmq]# rabbitmqctl set_user_tags root administrator
|
|
||||||
|
|
||||||
Setting tags for user "root" to [administrator] ...
|
|
||||||
[root@SnailClimb rabbitmq]#
|
|
||||||
[root@SnailClimb rabbitmq]# rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
|
|
||||||
Setting permissions for user "root" in vhost "/" ...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
再次访问:http://你的 ip 地址:15672/ ,输入用户名和密码:root root
|
|
||||||
|
|
||||||

|
|
@ -33,6 +33,105 @@ PS:也可能直接问什么是消息队列?消息队列就是一个使用队
|
|||||||
- **管理界面** : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。
|
- **管理界面** : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。
|
||||||
- **插件机制** : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。
|
- **插件机制** : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。
|
||||||
|
|
||||||
|
## RabbitMQ 核心概念?
|
||||||
|
|
||||||
|
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ 就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。
|
||||||
|
|
||||||
|
RabbitMQ 的整体模型架构如下:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
下面我会一一介绍上图中的一些概念。
|
||||||
|
|
||||||
|
### Producer(生产者) 和 Consumer(消费者)
|
||||||
|
|
||||||
|
- **Producer(生产者)** :生产消息的一方(邮件投递者)
|
||||||
|
- **Consumer(消费者)** :消费消息的一方(邮件收件人)
|
||||||
|
|
||||||
|
消息一般由 2 部分组成:**消息头**(或者说是标签 Label)和 **消息体**。消息体也可以称为 payLoad ,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。
|
||||||
|
|
||||||
|
### Exchange(交换器)
|
||||||
|
|
||||||
|
在 RabbitMQ 中,消息并不是直接被投递到 **Queue(消息队列)** 中的,中间还必须经过 **Exchange(交换器)** 这一层,**Exchange(交换器)** 会把我们的消息分配到对应的 **Queue(消息队列)** 中。
|
||||||
|
|
||||||
|
**Exchange(交换器)** 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 **Producer(生产者)** ,或许会被直接丢弃掉 。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
|
||||||
|
|
||||||
|
**RabbitMQ 的 Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略**:**direct(默认)**,**fanout**, **topic**, 和 **headers**,不同类型的 Exchange 转发消息的策略有所区别。这个会在介绍 **Exchange Types(交换器类型)** 的时候介绍到。
|
||||||
|
|
||||||
|
Exchange(交换器) 示意图如下:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
生产者将消息发给交换器的时候,一般会指定一个 **RoutingKey(路由键)**,用来指定这个消息的路由规则,而这个 **RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效**。
|
||||||
|
|
||||||
|
RabbitMQ 中通过 **Binding(绑定)** 将 **Exchange(交换器)** 与 **Queue(消息队列)** 关联起来,在绑定的时候一般会指定一个 **BindingKey(绑定建)** ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
|
||||||
|
|
||||||
|
Binding(绑定) 示意图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
|
||||||
|
|
||||||
|
### Queue(消息队列)
|
||||||
|
|
||||||
|
**Queue(消息队列)** 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
|
||||||
|
|
||||||
|
**RabbitMQ** 中消息只能存储在 **队列** 中,这一点和 **Kafka** 这种消息中间件相反。Kafka 将消息存储在 **topic(主题)** 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。 RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
|
||||||
|
|
||||||
|
**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
|
||||||
|
|
||||||
|
**RabbitMQ** 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
|
||||||
|
|
||||||
|
### Broker(消息中间件的服务节点)
|
||||||
|
|
||||||
|
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
|
||||||
|
|
||||||
|
下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整个流程。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这样图 1 中的一些关于 RabbitMQ 的基本概念我们就介绍完毕了,下面再来介绍一下 **Exchange Types(交换器类型)** 。
|
||||||
|
|
||||||
|
### Exchange Types(交换器类型)
|
||||||
|
|
||||||
|
RabbitMQ 常用的 Exchange Type 有 **fanout**、**direct**、**topic**、**headers** 这四种(AMQP 规范里还提到两种 Exchange Type,分别为 system 与 自定义,这里不予以描述)。
|
||||||
|
|
||||||
|
**1、fanout**
|
||||||
|
|
||||||
|
fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
|
||||||
|
|
||||||
|
**2、direct**
|
||||||
|
|
||||||
|
direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
以上图为例,如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
|
||||||
|
|
||||||
|
direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。
|
||||||
|
|
||||||
|
**3、topic**
|
||||||
|
|
||||||
|
前面讲到 direct 类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:
|
||||||
|
|
||||||
|
- RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
|
||||||
|
- BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串;
|
||||||
|
- BindingKey 中可以存在两种特殊字符串“\*”和“#”,用于做模糊匹配,其中“\*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
以上图为例:
|
||||||
|
|
||||||
|
- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2;
|
||||||
|
- 路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中;
|
||||||
|
- 路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中;
|
||||||
|
- 路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中;
|
||||||
|
- 路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。
|
||||||
|
|
||||||
|
**4、headers(不推荐)**
|
||||||
|
|
||||||
|
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
|
||||||
|
|
||||||
## AMQP 是什么?
|
## AMQP 是什么?
|
||||||
|
|
||||||
RabbitMQ 就是 AMQP 协议的 `Erlang` 的实现(当然 RabbitMQ 还支持 `STOMP2`、 `MQTT3` 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。
|
RabbitMQ 就是 AMQP 协议的 `Erlang` 的实现(当然 RabbitMQ 还支持 `STOMP2`、 `MQTT3` 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。
|
||||||
|
@ -1,460 +0,0 @@
|
|||||||
---
|
|
||||||
title: RocketMQ基础知识总结
|
|
||||||
category: 高性能
|
|
||||||
tag:
|
|
||||||
- RocketMQ
|
|
||||||
- 消息队列
|
|
||||||
---
|
|
||||||
|
|
||||||
> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
## 消息队列扫盲
|
|
||||||
|
|
||||||
消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧?
|
|
||||||
|
|
||||||
所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?**
|
|
||||||
|
|
||||||
### 消息队列为什么会出现?
|
|
||||||
|
|
||||||
消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
|
|
||||||
|
|
||||||
### 消息队列能用来干什么?
|
|
||||||
|
|
||||||
#### 异步
|
|
||||||
|
|
||||||
你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗?
|
|
||||||
|
|
||||||
很好 👍,你又提出了一个概念,**同步通信**。就比如现在业界使用比较多的 `Dubbo` 就是一个适用于各个系统之间同步通信的 `RPC` 框架。
|
|
||||||
|
|
||||||
我来举个 🌰 吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。
|
|
||||||
|
|
||||||
当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这样整个系统的调用链又变长了,整个时间就变成了 550ms。
|
|
||||||
|
|
||||||
当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。
|
|
||||||
|
|
||||||
我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦 😋😋😋” 咦~~~ 为了多吃点,真恶心。
|
|
||||||
|
|
||||||
然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。
|
|
||||||
|
|
||||||
最终我们从大妈手中接过饭菜然后去寻找座位了...
|
|
||||||
|
|
||||||
回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。
|
|
||||||
|
|
||||||
所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。
|
|
||||||
|
|
||||||
> 但是你需要注意的是,整个流程的时长是没变的,就像你仅仅告诉服务员要吃什么是不会影响到做面的速度的。
|
|
||||||
|
|
||||||
#### 解耦
|
|
||||||
|
|
||||||
回到最初同步调用的过程,我们写个伪代码简单概括一下。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这样改来改去是不是很麻烦,那么 **此时我们就用一个消息队列在中间进行解耦** 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 `result` ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 **“广播消息”** 来实现。
|
|
||||||
|
|
||||||
我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 **订阅** 特定的主题。比如我们这里的主题就可以叫做 `订票` ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 **生产消息到指定主题中** ,而 **消费者只需要关注从指定主题中拉取消息** 就行了。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。
|
|
||||||
|
|
||||||
#### 削峰
|
|
||||||
|
|
||||||
我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 **直接崩溃** 了?
|
|
||||||
|
|
||||||
短信业务又不是我们的主业务,我们能不能 **折中处理** 呢?如果我们把购买完成的信息发送到消息队列中,而短信系统 **尽自己所能地去消息队列中取消息和消费消息** ,即使处理速度慢一点也无所谓,只要我们的系统没有崩溃就行了。
|
|
||||||
|
|
||||||
留得江山在,还怕没柴烧?你敢说每次发送验证码的时候是一发你就收到了的么?
|
|
||||||
|
|
||||||
#### 消息队列能带来什么好处?
|
|
||||||
|
|
||||||
其实上面我已经说了。**异步、解耦、削峰。** 哪怕你上面的都没看懂也千万要记住这六个字,因为他不仅是消息队列的精华,更是编程和架构的精华。
|
|
||||||
|
|
||||||
#### 消息队列会带来副作用吗?
|
|
||||||
|
|
||||||
没有哪一门技术是“银弹”,消息队列也有它的副作用。
|
|
||||||
|
|
||||||
比如,本来好好的两个系统之间的调用,我中间加了个消息队列,如果消息队列挂了怎么办呢?是不是 **降低了系统的可用性** ?
|
|
||||||
|
|
||||||
那这样是不是要保证 HA(高可用)?是不是要搞集群?那么我 **整个系统的复杂度是不是上升了** ?
|
|
||||||
|
|
||||||
抛开上面的问题不讲,万一我发送方发送失败了,然后执行重试,这样就可能产生重复的消息。
|
|
||||||
|
|
||||||
或者我消费端处理失败了,请求重发,这样也会产生重复的消息。
|
|
||||||
|
|
||||||
对于一些微服务来说,消费重复消息会带来更大的麻烦,比如增加积分,这个时候我加了多次是不是对其他用户不公平?
|
|
||||||
|
|
||||||
那么,又 **如何解决重复消费消息的问题** 呢?
|
|
||||||
|
|
||||||
如果我们此时的消息需要保证严格的顺序性怎么办呢?比如生产者生产了一系列的有序消息(对一个 id 为 1 的记录进行删除增加修改),但是我们知道在发布订阅模型中,对于主题是无顺序的,那么这个时候就会导致对于消费者消费消息的时候没有按照生产者的发送顺序消费,比如这个时候我们消费的顺序为修改删除增加,如果该记录涉及到金额的话是不是会出大事情?
|
|
||||||
|
|
||||||
那么,又 **如何解决消息的顺序消费问题** 呢?
|
|
||||||
|
|
||||||
就拿我们上面所讲的分布式系统来说,用户购票完成之后是不是需要增加账户积分?在同一个系统中我们一般会使用事务来进行解决,如果用 `Spring` 的话我们在上面伪代码中加入 `@Transactional` 注解就好了。但是在不同系统中如何保证事务呢?总不能这个系统我扣钱成功了你那积分系统积分没加吧?或者说我这扣钱明明失败了,你那积分系统给我加了积分。
|
|
||||||
|
|
||||||
那么,又如何 **解决分布式事务问题** 呢?
|
|
||||||
|
|
||||||
我们刚刚说了,消息队列可以进行削峰操作,那如果我的消费者如果消费很慢或者生产者生产消息很快,这样是不是会将消息堆积在消息队列中?
|
|
||||||
|
|
||||||
那么,又如何 **解决消息堆积的问题** 呢?
|
|
||||||
|
|
||||||
可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊 😵?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
别急,办法总是有的。
|
|
||||||
|
|
||||||
## RocketMQ 是什么?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
哇,你个混蛋!上面给我抛出那么多问题,你现在又讲 `RocketMQ` ,还让不让人活了?!🤬
|
|
||||||
|
|
||||||
别急别急,话说你现在清楚 `MQ` 的构造吗,我还没讲呢,我们先搞明白 `MQ` 的内部构造,再来看看如何解决上面的一系列问题吧,不过你最好带着问题去阅读和了解喔。
|
|
||||||
|
|
||||||
`RocketMQ` 是一个 **队列模型** 的消息中间件,具有**高性能、高可靠、高实时、分布式** 的特点。它是一个采用 `Java` 语言开发的分布式的消息系统,由阿里巴巴团队开发,在 2016 年底贡献给 `Apache`,成为了 `Apache` 的一个顶级项目。 在阿里内部,`RocketMQ` 很好地服务了集团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过 `RocketMQ` 流转。
|
|
||||||
|
|
||||||
废话不多说,想要了解 `RocketMQ` 历史的同学可以自己去搜寻资料。听完上面的介绍,你只要知道 `RocketMQ` 很快、很牛、而且经历过双十一的实践就行了!
|
|
||||||
|
|
||||||
## 队列模型和主题模型
|
|
||||||
|
|
||||||
在谈 `RocketMQ` 的技术架构之前,我们先来了解一下两个名词概念——**队列模型** 和 **主题模型** 。
|
|
||||||
|
|
||||||
首先我问一个问题,消息队列为什么要叫消息队列?
|
|
||||||
|
|
||||||
你可能觉得很弱智,这玩意不就是存放消息的队列嘛?不叫消息队列叫什么?
|
|
||||||
|
|
||||||
的确,早期的消息中间件是通过 **队列** 这一模型来实现的,可能是历史原因,我们都习惯把消息中间件成为消息队列。
|
|
||||||
|
|
||||||
但是,如今例如 `RocketMQ` 、`Kafka` 这些优秀的消息中间件不仅仅是通过一个 **队列** 来实现消息存储的。
|
|
||||||
|
|
||||||
### 队列模型
|
|
||||||
|
|
||||||
就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列。。。我画一张图给大家理解。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
在一开始我跟你提到了一个 **“广播”** 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。
|
|
||||||
|
|
||||||
当然你可以让 `Producer` 生产消息放入多个队列中,然后每个队列去对应每一个消费者。问题是可以解决,创建多个队列并且复制多份消息是会很影响资源和性能的。而且,这样子就会导致生产者需要知道具体消费者个数然后去复制对应数量的消息队列,这就违背我们消息中间件的 **解耦** 这一原则。
|
|
||||||
|
|
||||||
### 主题模型
|
|
||||||
|
|
||||||
那么有没有好的方法去解决这一个问题呢?有,那就是 **主题模型** 或者可以称为 **发布订阅模型** 。
|
|
||||||
|
|
||||||
> 感兴趣的同学可以去了解一下设计模式里面的观察者模式并且手动实现一下,我相信你会有所收获的。
|
|
||||||
|
|
||||||
在主题模型中,消息的生产者称为 **发布者(Publisher)** ,消息的消费者称为 **订阅者(Subscriber)** ,存放消息的容器称为 **主题(Topic)** 。
|
|
||||||
|
|
||||||
其中,发布者将消息发送到指定主题中,订阅者需要 **提前订阅主题** 才能接受特定主题的消息。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### RocketMQ 中的消息模型
|
|
||||||
|
|
||||||
`RocketMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀!
|
|
||||||
|
|
||||||
其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 `Kafka` 中的 **分区** ,`RocketMQ` 中的 **队列** ,`RabbitMQ` 中的 `Exchange` 。我们可以理解为 **主题模型/发布订阅模型** 就是一个标准,那些中间件只不过照着这个标准去实现而已。
|
|
||||||
|
|
||||||
所以,`RocketMQ` 中的 **主题模型** 到底是如何实现的呢?首先我画一张图,大家尝试着去理解一下。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
我们可以看到在整个图中有 `Producer Group` 、`Topic` 、`Consumer Group` 三个角色,我来分别介绍一下他们。
|
|
||||||
|
|
||||||
- `Producer Group` 生产者组: 代表某一类的生产者,比如我们有多个秒杀系统作为生产者,这多个合在一起就是一个 `Producer Group` 生产者组,它们一般生产相同的消息。
|
|
||||||
- `Consumer Group` 消费者组: 代表某一类的消费者,比如我们有多个短信系统作为消费者,这多个合在一起就是一个 `Consumer Group` 消费者组,它们一般消费相同的消息。
|
|
||||||
- `Topic` 主题: 代表一类消息,比如订单消息,物流消息等等。
|
|
||||||
|
|
||||||
你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。
|
|
||||||
|
|
||||||
每个主题中都有多个队列(分布在不同的 `Broker`中,如果是集群的话,`Broker`又分布在不同的服务器中),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consumer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。
|
|
||||||
|
|
||||||
当然也可以消费者个数小于队列个数,只不过不太建议。如下图。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**每个消费组在每个队列上维护一个消费位置** ,为什么呢?
|
|
||||||
|
|
||||||
因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 **消费位移(offset)** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
可能你还有一个问题,**为什么一个主题中需要维护多个队列** ?
|
|
||||||
|
|
||||||
答案是 **提高并发能力** 。的确,每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 **发布订阅模式** 。如下图。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 `Consumer` 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多。
|
|
||||||
|
|
||||||
所以总结来说,`RocketMQ` 通过**使用在一个 `Topic` 中配置多个队列并且每个队列维护每个消费者组的消费位置** 实现了 **主题模式/发布订阅模式** 。
|
|
||||||
|
|
||||||
## RocketMQ 的架构图
|
|
||||||
|
|
||||||
讲完了消息模型,我们理解起 `RocketMQ` 的技术架构起来就容易多了。
|
|
||||||
|
|
||||||
`RocketMQ` 技术架构中有四大角色 `NameServer` 、`Broker` 、`Producer` 、`Consumer` 。我来向大家分别解释一下这四个角色是干啥的。
|
|
||||||
|
|
||||||
- `Broker`: 主要负责消息的存储、投递和查询以及服务高可用保证。说白了就是消息队列服务器嘛,生产者生产消息到 `Broker` ,消费者从 `Broker` 拉取消息并消费。
|
|
||||||
|
|
||||||
这里,我还得普及一下关于 `Broker` 、`Topic` 和 队列的关系。上面我讲解了 `Topic` 和队列的关系——一个 `Topic` 中存在多个队列,那么这个 `Topic` 和队列存放在哪呢?
|
|
||||||
|
|
||||||
**一个 `Topic` 分布在多个 `Broker`上,一个 `Broker` 可以配置多个 `Topic` ,它们是多对多的关系**。
|
|
||||||
|
|
||||||
如果某个 `Topic` 消息量很大,应该给它多配置几个队列(上文中提到了提高并发能力),并且 **尽量多分布在不同 `Broker` 上,以减轻某个 `Broker` 的压力** 。
|
|
||||||
|
|
||||||
`Topic` 消息量都比较均匀的情况下,如果某个 `broker` 上的队列越多,则该 `broker` 压力越大。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 所以说我们需要配置多个 Broker。
|
|
||||||
|
|
||||||
- `NameServer`: 不知道你们有没有接触过 `ZooKeeper` 和 `Spring Cloud` 中的 `Eureka` ,它其实也是一个 **注册中心** ,主要提供两个功能:**Broker 管理** 和 **路由信息管理** 。说白了就是 `Broker` 会将自己的信息注册到 `NameServer` 中,此时 `NameServer` 就存放了很多 `Broker` 的信息(Broker 的路由表),消费者和生产者就从 `NameServer` 中获取路由表然后照着路由表的信息和对应的 `Broker` 进行通信(生产者和消费者定期会向 `NameServer` 去查询相关的 `Broker` 的信息)。
|
|
||||||
|
|
||||||
- `Producer`: 消息发布的角色,支持分布式集群方式部署。说白了就是生产者。
|
|
||||||
|
|
||||||
- `Consumer`: 消息消费的角色,支持分布式集群方式部署。支持以 push 推,pull 拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
|
|
||||||
|
|
||||||
听完了上面的解释你可能会觉得,这玩意好简单。不就是这样的么?
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
嗯?你可能会发现一个问题,这老家伙 `NameServer` 干啥用的,这不多余吗?直接 `Producer` 、`Consumer` 和 `Broker` 直接进行生产消息,消费消息不就好了么?
|
|
||||||
|
|
||||||
但是,我们上文提到过 `Broker` 是需要保证高可用的,如果整个系统仅仅靠着一个 `Broker` 来维持的话,那么这个 `Broker` 的压力会不会很大?所以我们需要使用多个 `Broker` 来保证 **负载均衡** 。
|
|
||||||
|
|
||||||
如果说,我们的消费者和生产者直接和多个 `Broker` 相连,那么当 `Broker` 修改的时候必定会牵连着每个生产者和消费者,这样就会产生耦合问题,而 `NameServer` 注册中心就是用来解决这个问题的。
|
|
||||||
|
|
||||||
> 如果还不是很理解的话,可以去看我介绍 `Spring Cloud` 的那篇文章,其中介绍了 `Eureka` 注册中心。
|
|
||||||
|
|
||||||
当然,`RocketMQ` 中的技术架构肯定不止前面那么简单,因为上面图中的四个角色都是需要做集群的。我给出一张官网的架构图,大家尝试理解一下。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
其实和我们最开始画的那张乞丐版的架构图也没什么区别,主要是一些细节上的差别。听我细细道来 🤨。
|
|
||||||
|
|
||||||
第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,` salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。
|
|
||||||
|
|
||||||
第二、为了保证 `HA` ,我们的 `NameServer` 也做了集群部署,但是请注意它是 **去中心化** 的。也就意味着它没有主节点,你可以很明显地看出 `NameServer` 的所有节点是没有进行 `Info Replicate` 的,在 `RocketMQ` 中是通过 **单个 Broker 和所有 NameServer 保持长连接** ,并且在每隔 30 秒 `Broker` 会向所有 `Nameserver` 发送心跳,心跳包含了自身的 `Topic` 配置信息,这个步骤就对应这上面的 `Routing Info` 。
|
|
||||||
|
|
||||||
第三、在生产者需要向 `Broker` 发送消息的时候,**需要先从 `NameServer` 获取关于 `Broker` 的路由信息**,然后通过 **轮询** 的方法去向每个队列中生产数据以达到 **负载均衡** 的效果。
|
|
||||||
|
|
||||||
第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。
|
|
||||||
|
|
||||||
## 如何解决 顺序消费、重复消费
|
|
||||||
|
|
||||||
其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。
|
|
||||||
|
|
||||||
在上面我介绍 `RocketMQ` 的技术架构的时候我已经向你展示了 **它是如何保证高可用的** ,这里不涉及运维方面的搭建,如果你感兴趣可以自己去官网上照着例子搭建属于你自己的 `RocketMQ` 集群。
|
|
||||||
|
|
||||||
> 其实 `Kafka` 的架构基本和 `RocketMQ` 类似,只是它注册中心使用了 `Zookeeper` 、它的 **分区** 就相当于 `RocketMQ` 中的 **队列** 。还有一些小细节不同会在后面提到。
|
|
||||||
|
|
||||||
### 顺序消费
|
|
||||||
|
|
||||||
在上面的技术架构介绍中,我们已经知道了 **`RocketMQ` 在主题上是无序的、它只有在队列层面才是保证有序** 的。
|
|
||||||
|
|
||||||
这又扯到两个概念——**普通顺序** 和 **严格顺序** 。
|
|
||||||
|
|
||||||
所谓普通顺序是指 消费者通过 **同一个消费队列收到的消息是有顺序的** ,不同消息队列收到的消息则可能是无顺序的。普通顺序消息在 `Broker` **重启情况下不会保证消息顺序性** (短暂时间) 。
|
|
||||||
|
|
||||||
所谓严格顺序是指 消费者收到的 **所有消息** 均是有顺序的。严格顺序消息 **即使在异常情况下也会保证消息的顺序性** 。
|
|
||||||
|
|
||||||
但是,严格顺序看起来虽好,实现它可会付出巨大的代价。如果你使用严格顺序模式,`Broker` 集群中只要有一台机器不可用,则整个集群都不可用。你还用啥?现在主要场景也就在 `binlog` 同步。
|
|
||||||
|
|
||||||
一般而言,我们的 `MQ` 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。
|
|
||||||
|
|
||||||
那么,我们现在使用了 **普通顺序模式** ,我们从上面学习知道了在 `Producer` 生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。那么如果此时我有几个消息分别是同一个订单的创建、支付、发货,在轮询的策略下这 **三个消息会被发送到不同队列** ,因为在不同的队列此时就无法使用 `RocketMQ` 带来的队列有序特性来保证消息有序性了。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
那么,怎么解决呢?
|
|
||||||
|
|
||||||
其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 **Hash 取模法** 来保证同一个订单在同一个队列中就行了。
|
|
||||||
|
|
||||||
### 重复消费
|
|
||||||
|
|
||||||
emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。比如说,这个时候我们有一个订单的处理积分的系统,每当来一个消息的时候它就负责为创建这个订单的用户的积分加上相应的数值。可是有一次,消息队列发送给订单系统 FrancisQ 的订单信息,其要求是给 FrancisQ 的积分加上 500。但是积分系统在收到 FrancisQ 的订单信息处理完成之后返回给消息队列处理成功的信息的时候出现了网络波动(当然还有很多种情况,比如 Broker 意外重启等等),这条回应没有发送成功。
|
|
||||||
|
|
||||||
那么,消息队列没收到积分系统的回应会不会尝试重发这个消息?问题就来了,我再发这个消息,万一它又给 FrancisQ 的账户加上 500 积分怎么办呢?
|
|
||||||
|
|
||||||
所以我们需要给我们的消费者实现 **幂等** ,也就是对同一个消息的处理结果,执行多少次都不变。
|
|
||||||
|
|
||||||
那么如何给业务实现幂等呢?这个还是需要结合具体的业务的。你可以使用 **写入 `Redis`** 来保证,因为 `Redis` 的 `key` 和 `value` 就是天然支持幂等的。当然还有使用 **数据库插入法** ,基于数据库的唯一键来保证重复数据不会被插入多条。
|
|
||||||
|
|
||||||
不过最主要的还是需要 **根据特定场景使用特定的解决方案** ,你要知道你的消息消费是否是完全不可重复消费还是可以忍受重复消费的,然后再选择强校验和弱校验的方式。毕竟在 CS 领域还是很少有技术银弹的说法。
|
|
||||||
|
|
||||||
而在整个互联网领域,幂等不仅仅适用于消息队列的重复消费问题,这些实现幂等的方法,也同样适用于,**在其他场景中来解决重复请求或者重复调用的问题** 。比如将 HTTP 服务设计成幂等的,**解决前端或者 APP 重复提交表单数据的问题** ,也可以将一个微服务设计成幂等的,解决 `RPC` 框架自动重试导致的 **重复调用问题** 。
|
|
||||||
|
|
||||||
## 分布式事务
|
|
||||||
|
|
||||||
如何解释分布式事务呢?事务大家都知道吧?**要么都执行要么都不执行** 。在同一个系统中我们可以轻松地实现事务,但是在分布式架构中,我们有很多服务是部署在不同系统之间的,而不同服务之间又需要进行调用。比如此时我下订单然后增加积分,如果保证不了分布式事务的话,就会出现 A 系统下了订单,但是 B 系统增加积分失败或者 A 系统没有下订单,B 系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。
|
|
||||||
|
|
||||||
那么,如何去解决这个问题呢?
|
|
||||||
|
|
||||||
如今比较常见的分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,**都不是完美的解决方案**。
|
|
||||||
|
|
||||||
在 `RocketMQ` 中使用的是 **事务消息加上事务反查机制** 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
在第一步发送的 half 消息 ,它的意思是 **在事务提交之前,对于消费者来说,这个消息是不可见的** 。
|
|
||||||
|
|
||||||
> 那么,如何做到写入消息但是对用户不可见呢?RocketMQ 事务消息的做法是:如果消息是 half 消息,将备份原消息的主题与消息消费队列,然后 **改变主题** 为 RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费 half 类型的消息,**然后 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费**,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。
|
|
||||||
|
|
||||||
你可以试想一下,如果没有从第 5 步开始的 **事务反查机制** ,如果出现网路波动第 4 步没有发送成功,这样就会产生 MQ 不知道是不是需要给消费者消费的问题,他就像一个无头苍蝇一样。在 `RocketMQ` 中就是使用的上述的事务反查来解决的,而在 `Kafka` 中通常是直接抛出一个异常让用户来自行解决。
|
|
||||||
|
|
||||||
你还需要注意的是,在 `MQ Server` 指向系统 B 的操作已经和系统 A 不相关了,也就是说在消息队列中的分布式事务是——**本地事务和存储消息到消息队列才是同一个事务**。这样也就产生了事务的**最终一致性**,因为整个过程是异步的,**每个系统只要保证它自己那一部分的事务就行了**。
|
|
||||||
|
|
||||||
## 消息堆积问题
|
|
||||||
|
|
||||||
在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢?
|
|
||||||
|
|
||||||
其实这个问题可以将它广义化,因为产生消息堆积的根源其实就只有两个——生产者生产太快或者消费者消费太慢。
|
|
||||||
|
|
||||||
我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 **限流降级** 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 **是否是消费者出现了大量的消费错误** ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
|
|
||||||
|
|
||||||
> 当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 **同时你还需要增加每个主题的队列数量** 。
|
|
||||||
>
|
|
||||||
> 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 回溯消费
|
|
||||||
|
|
||||||
回溯消费是指 `Consumer` 已经消费成功的消息,由于业务上需求需要重新消费,在`RocketMQ` 中, `Broker` 在向`Consumer` 投递成功消息后,**消息仍然需要保留** 。并且重新消费一般是按照时间维度,例如由于 `Consumer` 系统故障,恢复后需要重新消费 1 小时前的数据,那么 `Broker` 要提供一种机制,可以按照时间维度来回退消费进度。`RocketMQ` 支持按照时间回溯消费,时间维度精确到毫秒。
|
|
||||||
|
|
||||||
这是官方文档的解释,我直接照搬过来就当科普了 😁😁😁。
|
|
||||||
|
|
||||||
## RocketMQ 的刷盘机制
|
|
||||||
|
|
||||||
上面我讲了那么多的 `RocketMQ` 的架构和设计原理,你有没有好奇
|
|
||||||
|
|
||||||
在 `Topic` 中的 **队列是以什么样的形式存在的?**
|
|
||||||
|
|
||||||
**队列中的消息又是如何进行存储持久化的呢?**
|
|
||||||
|
|
||||||
我在上文中提到的 **同步刷盘** 和 **异步刷盘** 又是什么呢?它们会给持久化带来什么样的影响呢?
|
|
||||||
|
|
||||||
下面我将给你们一一解释。
|
|
||||||
|
|
||||||
### 同步刷盘和异步刷盘
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
如上图所示,在同步刷盘中需要等待一个刷盘成功的 `ACK` ,同步刷盘对 `MQ` 消息可靠性来说是一种不错的保障,但是 **性能上会有较大影响** ,一般地适用于金融等特定业务场景。
|
|
||||||
|
|
||||||
而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行, **降低了读写延迟** ,提高了 `MQ` 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
|
|
||||||
|
|
||||||
一般地,**异步刷盘只有在 `Broker` 意外宕机的时候会丢失部分数据**,你可以设置 `Broker` 的参数 `FlushDiskType` 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
|
|
||||||
|
|
||||||
### 同步复制和异步复制
|
|
||||||
|
|
||||||
上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 `Borker` 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。
|
|
||||||
|
|
||||||
- 同步复制: 也叫 “同步双写”,也就是说,**只有消息同步双写到主从节点上时才返回写入成功** 。
|
|
||||||
- 异步复制: **消息写入主节点之后就直接返回写入成功** 。
|
|
||||||
|
|
||||||
然而,很多事情是没有完美的方案的,就比如我们进行消息写入的节点越多就更能保证消息的可靠性,但是随之的性能也会下降,所以需要程序员根据特定业务场景去选择适应的主从复制方案。
|
|
||||||
|
|
||||||
那么,**异步复制会不会也像异步刷盘那样影响消息的可靠性呢?**
|
|
||||||
|
|
||||||
答案是不会的,因为两者就是不同的概念,对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了 **可用性** 。为什么呢?其主要原因**是 `RocketMQ` 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了**。
|
|
||||||
|
|
||||||
比如这个时候采用异步复制的方式,在主节点还未发送完需要同步的消息的时候主节点挂掉了,这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了,**消费者可以自动切换到从节点进行消费**(仅仅是消费),所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况,降低了可用性,而当主节点重启之后,从节点那部分未来得及复制的消息还会继续复制。
|
|
||||||
|
|
||||||
在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?**一个主从不行那就多个主从的呗**,别忘了在我们最初的架构图中,每个 `Topic` 是分布在不同 `Broker` 中的。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
但是这种复制方式同样也会带来一个问题,那就是无法保证 **严格顺序** 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 `Topic` 下的队列来保证顺序性的。如果此时我们主节点 A 负责的是订单 A 的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点 A 的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了。
|
|
||||||
|
|
||||||
而在 `RocketMQ` 中采用了 `Dledger` 解决这个问题。他要求在写入消息的时候,要求**至少消息复制到半数以上的节点之后**,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解。
|
|
||||||
|
|
||||||
> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制半数以上节点的效率和直接异步复制还是有一定的差距的。
|
|
||||||
|
|
||||||
### 存储机制
|
|
||||||
|
|
||||||
还记得上面我们一开始的三个问题吗?到这里第三个问题已经解决了。
|
|
||||||
|
|
||||||
但是,在 `Topic` 中的 **队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢?** 还未解决,其实这里涉及到了 `RocketMQ` 是如何设计它的存储结构了。我首先想大家介绍 `RocketMQ` 消息存储架构中的三大角色——`CommitLog` 、`ConsumeQueue` 和 `IndexFile` 。
|
|
||||||
|
|
||||||
- `CommitLog`: **消息主体以及元数据的存储主体**,存储 `Producer` 端写入的消息主体内容,消息内容不是定长的。单个文件大小默认 1G ,文件名长度为 20 位,左边补零,剩余为起始偏移量,比如 00000000000000000000 代表了第一个文件,起始偏移量为 0,文件大小为 1G=1073741824;当第一个文件写满了,第二个文件为 00000000001073741824,起始偏移量为 1073741824,以此类推。消息主要是**顺序写入日志文件**,当文件满了,写入下一个文件。
|
|
||||||
- `ConsumeQueue`: 消息消费队列,**引入的目的主要是提高消息消费的性能**(我们再前面也讲了),由于`RocketMQ` 是基于主题 `Topic` 的订阅模式,消息消费是针对主题进行的,如果要遍历 `commitlog` 文件中根据 `Topic` 检索消息是非常低效的。`Consumer` 即可根据 `ConsumeQueue` 来查找待消费的消息。其中,`ConsumeQueue`(逻辑消费队列)**作为消费消息的索引**,保存了指定 `Topic` 下的队列消息在 `CommitLog` 中的**起始物理偏移量 `offset` **,消息大小 `size` 和消息 `Tag` 的 `HashCode` 值。**`consumequeue` 文件可以看成是基于 `topic` 的 `commitlog` 索引文件**,故 `consumequeue` 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样 `consumequeue` 文件采取定长设计,每一个条目共 20 个字节,分别为 8 字节的 `commitlog` 物理偏移量、4 字节的消息长度、8 字节 tag `hashcode`,单个文件由 30W 个条目组成,可以像数组一样随机访问每一个条目,每个 `ConsumeQueue`文件大小约 5.72M;
|
|
||||||
- `IndexFile`: `IndexFile`(索引文件)提供了一种可以通过 key 或时间区间来查询消息的方法。这里只做科普不做详细介绍。
|
|
||||||
|
|
||||||
总结来说,整个消息存储的结构,最主要的就是 `CommitLoq` 和 `ConsumeQueue` 。而 `ConsumeQueue` 你可以大概理解为 `Topic` 中的队列。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RockeMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。
|
|
||||||
|
|
||||||
而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。
|
|
||||||
|
|
||||||
所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号\*索引固定⻓度 20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。
|
|
||||||
|
|
||||||
讲到这里,你可能对 `RockeMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。
|
|
||||||
|
|
||||||
> 如果上面没看懂的读者一定要认真看下面的流程分析!
|
|
||||||
|
|
||||||
首先,在最上面的那一块就是我刚刚讲的你现在可以直接 **把 `ConsumerQueue` 理解为 `Queue`**。
|
|
||||||
|
|
||||||
在图中最左边说明了红色方块代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic` 、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog**。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和 tag 的 hash 值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。
|
|
||||||
|
|
||||||
上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。
|
|
||||||
|
|
||||||
因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考 🤔🤔 一下吧。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
为什么 `CommitLog` 文件要设计成固定大小的长度呢?提醒:**内存映射机制**。
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
总算把这篇博客写完了。我讲的你们还记得吗 😅?
|
|
||||||
|
|
||||||
这篇文章中我主要想大家介绍了
|
|
||||||
|
|
||||||
1. 消息队列出现的原因
|
|
||||||
2. 消息队列的作用(异步,解耦,削峰)
|
|
||||||
3. 消息队列带来的一系列问题(消息堆积、重复消费、顺序消费、分布式事务等等)
|
|
||||||
4. 消息队列的两种消息模型——队列和主题模式
|
|
||||||
5. 分析了 `RocketMQ` 的技术架构(`NameServer` 、`Broker` 、`Producer` 、`Comsumer`)
|
|
||||||
6. 结合 `RocketMQ` 回答了消息队列副作用的解决方案
|
|
||||||
7. 介绍了 `RocketMQ` 的存储机制和刷盘策略。
|
|
||||||
|
|
||||||
等等。。。
|
|
||||||
|
|
||||||
> 如果喜欢可以点赞哟 👍👍👍。
|
|
@ -6,198 +6,453 @@ tag:
|
|||||||
- 消息队列
|
- 消息队列
|
||||||
---
|
---
|
||||||
|
|
||||||
> 本文来自读者 [PR](https://github.com/Snailclimb/JavaGuide/pull/291)。
|
> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd)
|
||||||
|
|
||||||
## 1 单机版消息中心
|
## 消息队列扫盲
|
||||||
|
|
||||||
一个消息中心,最基本的需要支持多生产者、多消费者,例如下:
|
消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧?
|
||||||
|
|
||||||
```java
|
所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?**
|
||||||
class Scratch {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
### 消息队列为什么会出现?
|
||||||
// 实际中会有 nameserver 服务来找到 broker 具体位置以及 broker 主从信息
|
|
||||||
Broker broker = new Broker();
|
|
||||||
Producer producer1 = new Producer();
|
|
||||||
producer1.connectBroker(broker);
|
|
||||||
Producer producer2 = new Producer();
|
|
||||||
producer2.connectBroker(broker);
|
|
||||||
|
|
||||||
Consumer consumer1 = new Consumer();
|
消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
|
||||||
consumer1.connectBroker(broker);
|
|
||||||
Consumer consumer2 = new Consumer();
|
|
||||||
consumer2.connectBroker(broker);
|
|
||||||
|
|
||||||
for (int i = 0; i < 2; i++) {
|
### 消息队列能用来干什么?
|
||||||
producer1.asyncSendMsg("producer1 send msg" + i);
|
|
||||||
producer2.asyncSendMsg("producer2 send msg" + i);
|
|
||||||
}
|
|
||||||
System.out.println("broker has msg:" + broker.getAllMagByDisk());
|
|
||||||
|
|
||||||
for (int i = 0; i < 1; i++) {
|
#### 异步
|
||||||
System.out.println("consumer1 consume msg:" + consumer1.syncPullMsg());
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
System.out.println("consumer2 consume msg:" + consumer2.syncPullMsg());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗?
|
||||||
|
|
||||||
class Producer {
|
很好 👍,你又提出了一个概念,**同步通信**。就比如现在业界使用比较多的 `Dubbo` 就是一个适用于各个系统之间同步通信的 `RPC` 框架。
|
||||||
|
|
||||||
private Broker broker;
|
我来举个 🌰 吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。
|
||||||
|
|
||||||
public void connectBroker(Broker broker) {
|

|
||||||
this.broker = broker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void asyncSendMsg(String msg) {
|
我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。
|
||||||
if (broker == null) {
|
|
||||||
throw new RuntimeException("please connect broker first");
|
|
||||||
}
|
|
||||||
new Thread(() -> {
|
|
||||||
broker.sendMsg(msg);
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Consumer {
|
当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢?
|
||||||
private Broker broker;
|
|
||||||
|
|
||||||
public void connectBroker(Broker broker) {
|

|
||||||
this.broker = broker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String syncPullMsg() {
|
这样整个系统的调用链又变长了,整个时间就变成了 550ms。
|
||||||
return broker.getMsg();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。
|
||||||
|
|
||||||
class Broker {
|
我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦 😋😋😋” 咦~~~ 为了多吃点,真恶心。
|
||||||
|
|
||||||
// 对应 RocketMQ 中 MessageQueue,默认情况下 1 个 Topic 包含 4 个 MessageQueue
|
然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。
|
||||||
private LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue(Integer.MAX_VALUE);
|
|
||||||
|
|
||||||
// 实际发送消息到 broker 服务器使用 Netty 发送
|
最终我们从大妈手中接过饭菜然后去寻找座位了...
|
||||||
public void sendMsg(String msg) {
|
|
||||||
try {
|
|
||||||
messageQueue.put(msg);
|
|
||||||
// 实际会同步或异步落盘,异步落盘使用的定时任务定时扫描落盘
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
|
|
||||||
}
|
回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。
|
||||||
}
|
|
||||||
|
|
||||||
public String getMsg() {
|

|
||||||
try {
|
|
||||||
return messageQueue.take();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
|
|
||||||
}
|
那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAllMagByDisk() {
|
所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。
|
||||||
StringBuilder sb = new StringBuilder("\n");
|
|
||||||
messageQueue.iterator().forEachRemaining((msg) -> {
|
|
||||||
sb.append(msg + "\n");
|
|
||||||
});
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
问题:
|

|
||||||
|
|
||||||
1. 没有实现真正执行消息存储落盘
|
这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。
|
||||||
2. 没有实现 NameServer 去作为注册中心,定位服务
|
|
||||||
3. 使用 LinkedBlockingQueue 作为消息队列,注意,参数是无限大,在真正 RocketMQ 也是如此是无限大,理论上不会出现对进来的数据进行抛弃,但是会有内存泄漏问题(阿里巴巴开发手册也因为这个问题,建议我们使用自制线程池)
|
|
||||||
4. 没有使用多个队列(即多个 LinkedBlockingQueue),RocketMQ 的顺序消息是通过生产者和消费者同时使用同一个 MessageQueue 来实现,但是如果我们只有一个 MessageQueue,那我们天然就支持顺序消息
|
|
||||||
5. 没有使用 MappedByteBuffer 来实现文件映射从而使消息数据落盘非常的快(实际 RocketMQ 使用的是 FileChannel+DirectBuffer)
|
|
||||||
|
|
||||||
## 2 分布式消息中心
|
> 但是你需要注意的是,整个流程的时长是没变的,就像你仅仅告诉服务员要吃什么是不会影响到做面的速度的。
|
||||||
|
|
||||||
### 2.1 问题与解决
|
#### 解耦
|
||||||
|
|
||||||
#### 2.1.1 消息丢失的问题
|
回到最初同步调用的过程,我们写个伪代码简单概括一下。
|
||||||
|
|
||||||
1. 当你系统需要保证百分百消息不丢失,你可以使用生产者每发送一个消息,Broker 同步返回一个消息发送成功的反馈消息
|

|
||||||
2. 即每发送一个消息,同步落盘后才返回生产者消息发送成功,这样只要生产者得到了消息发送生成的返回,事后除了硬盘损坏,都可以保证不会消息丢失
|
|
||||||
3. 但是这同时引入了一个问题,同步落盘怎么才能快?
|
|
||||||
|
|
||||||
#### 2.1.2 同步落盘怎么才能快
|
那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码?
|
||||||
|
|
||||||
1. 使用 FileChannel + DirectBuffer 池,使用堆外内存,加快内存拷贝
|

|
||||||
2. 使用数据和索引分离,当消息需要写入时,使用 commitlog 文件顺序写,当需要定位某个消息时,查询 index 文件来定位,从而减少文件 IO 随机读写的性能损耗
|
|
||||||
|
|
||||||
#### 2.1.3 消息堆积的问题
|
如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用?
|
||||||
|
|
||||||
1. 后台定时任务每隔 72 小时,删除旧的没有使用过的消息信息
|

|
||||||
2. 根据不同的业务实现不同的丢弃任务,具体参考线程池的 AbortPolicy,例如 FIFO/LRU 等(RocketMQ 没有此策略)
|
|
||||||
3. 消息定时转移,或者对某些重要的 TAG 型(支付型)消息真正落库
|
|
||||||
|
|
||||||
#### 2.1.4 定时消息的实现
|
这样改来改去是不是很麻烦,那么 **此时我们就用一个消息队列在中间进行解耦** 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 `result` ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 **“广播消息”** 来实现。
|
||||||
|
|
||||||
1. 实际 RocketMQ 没有实现任意精度的定时消息,它只支持某些特定的时间精度的定时消息
|
我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 **订阅** 特定的主题。比如我们这里的主题就可以叫做 `订票` ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 **生产消息到指定主题中** ,而 **消费者只需要关注从指定主题中拉取消息** 就行了。
|
||||||
2. 实现定时消息的原理是:创建特定时间精度的 MessageQueue,例如生产者需要定时 1s 之后被消费者消费,你只需要将此消息发送到特定的 Topic,例如:MessageQueue-1 表示这个 MessageQueue 里面的消息都会延迟一秒被消费,然后 Broker 会在 1s 后发送到消费者消费此消息,使用 newSingleThreadScheduledExecutor 实现
|
|
||||||
|
|
||||||
#### 2.1.5 顺序消息的实现
|

|
||||||
|
|
||||||
1. 与定时消息同原理,生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息
|
> 如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。
|
||||||
2. 注意:同一个 MessageQueue 保证里面的消息是顺序消费的前提是:消费者是串行的消费该 MessageQueue,因为就算 MessageQueue 是顺序的,但是当并行消费时,还是会有顺序问题,但是串行消费也同时引入了两个问题:
|
|
||||||
> 1. 引入锁来实现串行
|
|
||||||
> 2. 前一个消费阻塞时后面都会被阻塞
|
|
||||||
|
|
||||||
#### 2.1.6 分布式消息的实现
|
#### 削峰
|
||||||
|
|
||||||
1. 需要前置知识:2PC
|
我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样?
|
||||||
2. RocketMQ4.3 起支持,原理为 2PC,即两阶段提交,prepared->commit/rollback
|
|
||||||
3. 生产者发送事务消息,假设该事务消息 Topic 为 Topic1-Trans,Broker 得到后首先更改该消息的 Topic 为 Topic1-Prepared,该 Topic1-Prepared 对消费者不可见。然后定时回调生产者的本地事务 A 执行状态,根据本地事务 A 执行状态,来是否将该消息修改为 Topic1-Commit 或 Topic1-Rollback,消费者就可以正常找到该事务消息或者不执行等
|
|
||||||
|
|
||||||
> 注意,就算是事务消息最后回滚了也不会物理删除,只会逻辑删除该消息
|

|
||||||
|
|
||||||
#### 2.1.7 消息的 push 实现
|
如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 **直接崩溃** 了?
|
||||||
|
|
||||||
1. 注意,RocketMQ 已经说了自己会有低延迟问题,其中就包括这个消息的 push 延迟问题
|
短信业务又不是我们的主业务,我们能不能 **折中处理** 呢?如果我们把购买完成的信息发送到消息队列中,而短信系统 **尽自己所能地去消息队列中取消息和消费消息** ,即使处理速度慢一点也无所谓,只要我们的系统没有崩溃就行了。
|
||||||
2. 因为这并不是真正的将消息主动的推送到消费者,而是 Broker 定时任务每 5s 将消息推送到消费者
|
|
||||||
3. pull 模式需要我们手动调用 consumer 拉消息,而 push 模式则只需要我们提供一个 listener 即可实现对消息的监听,而实际上,RocketMQ 的 push 模式是基于 pull 模式实现的,它没有实现真正的 push。
|
|
||||||
4. push 方式里,consumer 把轮询过程封装了,并注册 MessageListener 监听器,取到消息后,唤醒 MessageListener 的 consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。
|
|
||||||
|
|
||||||
#### 2.1.8 消息重复发送的避免
|
留得江山在,还怕没柴烧?你敢说每次发送验证码的时候是一发你就收到了的么?
|
||||||
|
|
||||||
1. RocketMQ 会出现消息重复发送的问题,因为在网络延迟的情况下,这种问题不可避免的发生,如果非要实现消息不可重复发送,那基本太难,因为网络环境无法预知,还会使程序复杂度加大,因此默认允许消息重复发送
|
#### 消息队列能带来什么好处?
|
||||||
2. RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息
|
|
||||||
3. 最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来判断是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费
|
|
||||||
4. 具体实现可以查询关于消息幂等消费的解决方案
|
|
||||||
|
|
||||||
#### 2.1.9 广播消费与集群消费
|
其实上面我已经说了。**异步、解耦、削峰。** 哪怕你上面的都没看懂也千万要记住这六个字,因为他不仅是消息队列的精华,更是编程和架构的精华。
|
||||||
|
|
||||||
1. 消息消费区别:广播消费,订阅该 Topic 的消息者们都会消费**每个**消息。集群消费,订阅该 Topic 的消息者们只会有一个去消费**某个**消息
|
#### 消息队列会带来副作用吗?
|
||||||
2. 消息落盘区别:具体表现在消息消费进度的保存上。广播消费,由于每个消费者都独立的去消费每个消息,因此每个消费者各自保存自己的消息消费进度。而集群消费下,订阅了某个 Topic,而旗下又有多个 MessageQueue,每个消费者都可能会去消费不同的 MessageQueue,因此总体的消费进度保存在 Broker 上集中的管理
|
|
||||||
|
|
||||||
#### 2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点?
|
没有哪一门技术是“银弹”,消息队列也有它的副作用。
|
||||||
|
|
||||||
1. ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致
|
比如,本来好好的两个系统之间的调用,我中间加了个消息队列,如果消息队列挂了怎么办呢?是不是 **降低了系统的可用性** ?
|
||||||
2. 另外,当有新的服务器加入时,NameServer 并不会立马通知到 Producer,而是由 Producer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决)
|
|
||||||
|
|
||||||
#### 2.1.11 其它
|
那这样是不是要保证 HA(高可用)?是不是要搞集群?那么我 **整个系统的复杂度是不是上升了** ?
|
||||||
|
|
||||||

|
抛开上面的问题不讲,万一我发送方发送失败了,然后执行重试,这样就可能产生重复的消息。
|
||||||
|
|
||||||
加分项咯
|
或者我消费端处理失败了,请求重发,这样也会产生重复的消息。
|
||||||
|
|
||||||
1. 包括组件通信间使用 Netty 的自定义协议
|
对于一些微服务来说,消费重复消息会带来更大的麻烦,比如增加积分,这个时候我加了多次是不是对其他用户不公平?
|
||||||
2. 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略)
|
|
||||||
3. 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤)
|
|
||||||
4. Broker 同步双写和异步双写中 Master 和 Slave 的交互
|
|
||||||
5. Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性。
|
|
||||||
|
|
||||||
## 3 参考
|
那么,又 **如何解决重复消费消息的问题** 呢?
|
||||||
|
|
||||||
1. 《RocketMQ 技术内幕》:https://blog.csdn.net/prestigeding/article/details/85233529
|
如果我们此时的消息需要保证严格的顺序性怎么办呢?比如生产者生产了一系列的有序消息(对一个 id 为 1 的记录进行删除增加修改),但是我们知道在发布订阅模型中,对于主题是无顺序的,那么这个时候就会导致对于消费者消费消息的时候没有按照生产者的发送顺序消费,比如这个时候我们消费的顺序为修改删除增加,如果该记录涉及到金额的话是不是会出大事情?
|
||||||
2. 十分钟入门 RocketMQ:https://developer.aliyun.com/article/66101
|
|
||||||
3. 分布式事务的种类以及 RocketMQ 支持的分布式消息:https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
|
那么,又 **如何解决消息的顺序消费问题** 呢?
|
||||||
4. 滴滴出行基于 RocketMQ 构建企业级消息队列服务的实践:https://yq.aliyun.com/articles/664608
|
|
||||||
5. 基于《RocketMQ 技术内幕》源码注释:https://github.com/LiWenGu/awesome-rocketmq
|
就拿我们上面所讲的分布式系统来说,用户购票完成之后是不是需要增加账户积分?在同一个系统中我们一般会使用事务来进行解决,如果用 `Spring` 的话我们在上面伪代码中加入 `@Transactional` 注解就好了。但是在不同系统中如何保证事务呢?总不能这个系统我扣钱成功了你那积分系统积分没加吧?或者说我这扣钱明明失败了,你那积分系统给我加了积分。
|
||||||
|
|
||||||
|
那么,又如何 **解决分布式事务问题** 呢?
|
||||||
|
|
||||||
|
我们刚刚说了,消息队列可以进行削峰操作,那如果我的消费者如果消费很慢或者生产者生产消息很快,这样是不是会将消息堆积在消息队列中?
|
||||||
|
|
||||||
|
那么,又如何 **解决消息堆积的问题** 呢?
|
||||||
|
|
||||||
|
可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊 😵?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
别急,办法总是有的。
|
||||||
|
|
||||||
|
## RocketMQ 是什么?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
哇,你个混蛋!上面给我抛出那么多问题,你现在又讲 `RocketMQ` ,还让不让人活了?!🤬
|
||||||
|
|
||||||
|
别急别急,话说你现在清楚 `MQ` 的构造吗,我还没讲呢,我们先搞明白 `MQ` 的内部构造,再来看看如何解决上面的一系列问题吧,不过你最好带着问题去阅读和了解喔。
|
||||||
|
|
||||||
|
`RocketMQ` 是一个 **队列模型** 的消息中间件,具有**高性能、高可靠、高实时、分布式** 的特点。它是一个采用 `Java` 语言开发的分布式的消息系统,由阿里巴巴团队开发,在 2016 年底贡献给 `Apache`,成为了 `Apache` 的一个顶级项目。 在阿里内部,`RocketMQ` 很好地服务了集团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过 `RocketMQ` 流转。
|
||||||
|
|
||||||
|
废话不多说,想要了解 `RocketMQ` 历史的同学可以自己去搜寻资料。听完上面的介绍,你只要知道 `RocketMQ` 很快、很牛、而且经历过双十一的实践就行了!
|
||||||
|
|
||||||
|
## 队列模型和主题模型是什么?
|
||||||
|
|
||||||
|
在谈 `RocketMQ` 的技术架构之前,我们先来了解一下两个名词概念——**队列模型** 和 **主题模型** 。
|
||||||
|
|
||||||
|
首先我问一个问题,消息队列为什么要叫消息队列?
|
||||||
|
|
||||||
|
你可能觉得很弱智,这玩意不就是存放消息的队列嘛?不叫消息队列叫什么?
|
||||||
|
|
||||||
|
的确,早期的消息中间件是通过 **队列** 这一模型来实现的,可能是历史原因,我们都习惯把消息中间件成为消息队列。
|
||||||
|
|
||||||
|
但是,如今例如 `RocketMQ` 、`Kafka` 这些优秀的消息中间件不仅仅是通过一个 **队列** 来实现消息存储的。
|
||||||
|
|
||||||
|
### 队列模型
|
||||||
|
|
||||||
|
就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列。。。我画一张图给大家理解。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在一开始我跟你提到了一个 **“广播”** 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。
|
||||||
|
|
||||||
|
当然你可以让 `Producer` 生产消息放入多个队列中,然后每个队列去对应每一个消费者。问题是可以解决,创建多个队列并且复制多份消息是会很影响资源和性能的。而且,这样子就会导致生产者需要知道具体消费者个数然后去复制对应数量的消息队列,这就违背我们消息中间件的 **解耦** 这一原则。
|
||||||
|
|
||||||
|
### 主题模型
|
||||||
|
|
||||||
|
那么有没有好的方法去解决这一个问题呢?有,那就是 **主题模型** 或者可以称为 **发布订阅模型** 。
|
||||||
|
|
||||||
|
> 感兴趣的同学可以去了解一下设计模式里面的观察者模式并且手动实现一下,我相信你会有所收获的。
|
||||||
|
|
||||||
|
在主题模型中,消息的生产者称为 **发布者(Publisher)** ,消息的消费者称为 **订阅者(Subscriber)** ,存放消息的容器称为 **主题(Topic)** 。
|
||||||
|
|
||||||
|
其中,发布者将消息发送到指定主题中,订阅者需要 **提前订阅主题** 才能接受特定主题的消息。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### RocketMQ 中的消息模型
|
||||||
|
|
||||||
|
`RocketMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀!
|
||||||
|
|
||||||
|
其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 `Kafka` 中的 **分区** ,`RocketMQ` 中的 **队列** ,`RabbitMQ` 中的 `Exchange` 。我们可以理解为 **主题模型/发布订阅模型** 就是一个标准,那些中间件只不过照着这个标准去实现而已。
|
||||||
|
|
||||||
|
所以,`RocketMQ` 中的 **主题模型** 到底是如何实现的呢?首先我画一张图,大家尝试着去理解一下。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们可以看到在整个图中有 `Producer Group` 、`Topic` 、`Consumer Group` 三个角色,我来分别介绍一下他们。
|
||||||
|
|
||||||
|
- `Producer Group` 生产者组: 代表某一类的生产者,比如我们有多个秒杀系统作为生产者,这多个合在一起就是一个 `Producer Group` 生产者组,它们一般生产相同的消息。
|
||||||
|
- `Consumer Group` 消费者组: 代表某一类的消费者,比如我们有多个短信系统作为消费者,这多个合在一起就是一个 `Consumer Group` 消费者组,它们一般消费相同的消息。
|
||||||
|
- `Topic` 主题: 代表一类消息,比如订单消息,物流消息等等。
|
||||||
|
|
||||||
|
你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。
|
||||||
|
|
||||||
|
每个主题中都有多个队列(分布在不同的 `Broker`中,如果是集群的话,`Broker`又分布在不同的服务器中),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consumer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。
|
||||||
|
|
||||||
|
当然也可以消费者个数小于队列个数,只不过不太建议。如下图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**每个消费组在每个队列上维护一个消费位置** ,为什么呢?
|
||||||
|
|
||||||
|
因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 **消费位移(offset)** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
可能你还有一个问题,**为什么一个主题中需要维护多个队列** ?
|
||||||
|
|
||||||
|
答案是 **提高并发能力** 。的确,每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 **发布订阅模式** 。如下图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 `Consumer` 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多。
|
||||||
|
|
||||||
|
所以总结来说,`RocketMQ` 通过**使用在一个 `Topic` 中配置多个队列并且每个队列维护每个消费者组的消费位置** 实现了 **主题模式/发布订阅模式** 。
|
||||||
|
|
||||||
|
## RocketMQ 的架构图
|
||||||
|
|
||||||
|
讲完了消息模型,我们理解起 `RocketMQ` 的技术架构起来就容易多了。
|
||||||
|
|
||||||
|
`RocketMQ` 技术架构中有四大角色 `NameServer` 、`Broker` 、`Producer` 、`Consumer` 。我来向大家分别解释一下这四个角色是干啥的。
|
||||||
|
|
||||||
|
- `Broker`: 主要负责消息的存储、投递和查询以及服务高可用保证。说白了就是消息队列服务器嘛,生产者生产消息到 `Broker` ,消费者从 `Broker` 拉取消息并消费。
|
||||||
|
|
||||||
|
这里,我还得普及一下关于 `Broker` 、`Topic` 和 队列的关系。上面我讲解了 `Topic` 和队列的关系——一个 `Topic` 中存在多个队列,那么这个 `Topic` 和队列存放在哪呢?
|
||||||
|
|
||||||
|
**一个 `Topic` 分布在多个 `Broker`上,一个 `Broker` 可以配置多个 `Topic` ,它们是多对多的关系**。
|
||||||
|
|
||||||
|
如果某个 `Topic` 消息量很大,应该给它多配置几个队列(上文中提到了提高并发能力),并且 **尽量多分布在不同 `Broker` 上,以减轻某个 `Broker` 的压力** 。
|
||||||
|
|
||||||
|
`Topic` 消息量都比较均匀的情况下,如果某个 `broker` 上的队列越多,则该 `broker` 压力越大。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 所以说我们需要配置多个 Broker。
|
||||||
|
|
||||||
|
- `NameServer`: 不知道你们有没有接触过 `ZooKeeper` 和 `Spring Cloud` 中的 `Eureka` ,它其实也是一个 **注册中心** ,主要提供两个功能:**Broker 管理** 和 **路由信息管理** 。说白了就是 `Broker` 会将自己的信息注册到 `NameServer` 中,此时 `NameServer` 就存放了很多 `Broker` 的信息(Broker 的路由表),消费者和生产者就从 `NameServer` 中获取路由表然后照着路由表的信息和对应的 `Broker` 进行通信(生产者和消费者定期会向 `NameServer` 去查询相关的 `Broker` 的信息)。
|
||||||
|
|
||||||
|
- `Producer`: 消息发布的角色,支持分布式集群方式部署。说白了就是生产者。
|
||||||
|
|
||||||
|
- `Consumer`: 消息消费的角色,支持分布式集群方式部署。支持以 push 推,pull 拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
|
||||||
|
|
||||||
|
听完了上面的解释你可能会觉得,这玩意好简单。不就是这样的么?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
嗯?你可能会发现一个问题,这老家伙 `NameServer` 干啥用的,这不多余吗?直接 `Producer` 、`Consumer` 和 `Broker` 直接进行生产消息,消费消息不就好了么?
|
||||||
|
|
||||||
|
但是,我们上文提到过 `Broker` 是需要保证高可用的,如果整个系统仅仅靠着一个 `Broker` 来维持的话,那么这个 `Broker` 的压力会不会很大?所以我们需要使用多个 `Broker` 来保证 **负载均衡** 。
|
||||||
|
|
||||||
|
如果说,我们的消费者和生产者直接和多个 `Broker` 相连,那么当 `Broker` 修改的时候必定会牵连着每个生产者和消费者,这样就会产生耦合问题,而 `NameServer` 注册中心就是用来解决这个问题的。
|
||||||
|
|
||||||
|
> 如果还不是很理解的话,可以去看我介绍 `Spring Cloud` 的那篇文章,其中介绍了 `Eureka` 注册中心。
|
||||||
|
|
||||||
|
当然,`RocketMQ` 中的技术架构肯定不止前面那么简单,因为上面图中的四个角色都是需要做集群的。我给出一张官网的架构图,大家尝试理解一下。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其实和我们最开始画的那张乞丐版的架构图也没什么区别,主要是一些细节上的差别。听我细细道来 🤨。
|
||||||
|
|
||||||
|
第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,` salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。
|
||||||
|
|
||||||
|
第二、为了保证 `HA` ,我们的 `NameServer` 也做了集群部署,但是请注意它是 **去中心化** 的。也就意味着它没有主节点,你可以很明显地看出 `NameServer` 的所有节点是没有进行 `Info Replicate` 的,在 `RocketMQ` 中是通过 **单个 Broker 和所有 NameServer 保持长连接** ,并且在每隔 30 秒 `Broker` 会向所有 `Nameserver` 发送心跳,心跳包含了自身的 `Topic` 配置信息,这个步骤就对应这上面的 `Routing Info` 。
|
||||||
|
|
||||||
|
第三、在生产者需要向 `Broker` 发送消息的时候,**需要先从 `NameServer` 获取关于 `Broker` 的路由信息**,然后通过 **轮询** 的方法去向每个队列中生产数据以达到 **负载均衡** 的效果。
|
||||||
|
|
||||||
|
第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。
|
||||||
|
|
||||||
|
## 如何解决顺序消费和重复消费?
|
||||||
|
|
||||||
|
其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。
|
||||||
|
|
||||||
|
在上面我介绍 `RocketMQ` 的技术架构的时候我已经向你展示了 **它是如何保证高可用的** ,这里不涉及运维方面的搭建,如果你感兴趣可以自己去官网上照着例子搭建属于你自己的 `RocketMQ` 集群。
|
||||||
|
|
||||||
|
> 其实 `Kafka` 的架构基本和 `RocketMQ` 类似,只是它注册中心使用了 `Zookeeper` 、它的 **分区** 就相当于 `RocketMQ` 中的 **队列** 。还有一些小细节不同会在后面提到。
|
||||||
|
|
||||||
|
### 顺序消费
|
||||||
|
|
||||||
|
在上面的技术架构介绍中,我们已经知道了 **`RocketMQ` 在主题上是无序的、它只有在队列层面才是保证有序** 的。
|
||||||
|
|
||||||
|
这又扯到两个概念——**普通顺序** 和 **严格顺序** 。
|
||||||
|
|
||||||
|
所谓普通顺序是指 消费者通过 **同一个消费队列收到的消息是有顺序的** ,不同消息队列收到的消息则可能是无顺序的。普通顺序消息在 `Broker` **重启情况下不会保证消息顺序性** (短暂时间) 。
|
||||||
|
|
||||||
|
所谓严格顺序是指 消费者收到的 **所有消息** 均是有顺序的。严格顺序消息 **即使在异常情况下也会保证消息的顺序性** 。
|
||||||
|
|
||||||
|
但是,严格顺序看起来虽好,实现它可会付出巨大的代价。如果你使用严格顺序模式,`Broker` 集群中只要有一台机器不可用,则整个集群都不可用。你还用啥?现在主要场景也就在 `binlog` 同步。
|
||||||
|
|
||||||
|
一般而言,我们的 `MQ` 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。
|
||||||
|
|
||||||
|
那么,我们现在使用了 **普通顺序模式** ,我们从上面学习知道了在 `Producer` 生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。那么如果此时我有几个消息分别是同一个订单的创建、支付、发货,在轮询的策略下这 **三个消息会被发送到不同队列** ,因为在不同的队列此时就无法使用 `RocketMQ` 带来的队列有序特性来保证消息有序性了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
那么,怎么解决呢?
|
||||||
|
|
||||||
|
其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 **Hash 取模法** 来保证同一个订单在同一个队列中就行了。
|
||||||
|
|
||||||
|
### 重复消费
|
||||||
|
|
||||||
|
emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。比如说,这个时候我们有一个订单的处理积分的系统,每当来一个消息的时候它就负责为创建这个订单的用户的积分加上相应的数值。可是有一次,消息队列发送给订单系统 FrancisQ 的订单信息,其要求是给 FrancisQ 的积分加上 500。但是积分系统在收到 FrancisQ 的订单信息处理完成之后返回给消息队列处理成功的信息的时候出现了网络波动(当然还有很多种情况,比如 Broker 意外重启等等),这条回应没有发送成功。
|
||||||
|
|
||||||
|
那么,消息队列没收到积分系统的回应会不会尝试重发这个消息?问题就来了,我再发这个消息,万一它又给 FrancisQ 的账户加上 500 积分怎么办呢?
|
||||||
|
|
||||||
|
所以我们需要给我们的消费者实现 **幂等** ,也就是对同一个消息的处理结果,执行多少次都不变。
|
||||||
|
|
||||||
|
那么如何给业务实现幂等呢?这个还是需要结合具体的业务的。你可以使用 **写入 `Redis`** 来保证,因为 `Redis` 的 `key` 和 `value` 就是天然支持幂等的。当然还有使用 **数据库插入法** ,基于数据库的唯一键来保证重复数据不会被插入多条。
|
||||||
|
|
||||||
|
不过最主要的还是需要 **根据特定场景使用特定的解决方案** ,你要知道你的消息消费是否是完全不可重复消费还是可以忍受重复消费的,然后再选择强校验和弱校验的方式。毕竟在 CS 领域还是很少有技术银弹的说法。
|
||||||
|
|
||||||
|
而在整个互联网领域,幂等不仅仅适用于消息队列的重复消费问题,这些实现幂等的方法,也同样适用于,**在其他场景中来解决重复请求或者重复调用的问题** 。比如将 HTTP 服务设计成幂等的,**解决前端或者 APP 重复提交表单数据的问题** ,也可以将一个微服务设计成幂等的,解决 `RPC` 框架自动重试导致的 **重复调用问题** 。
|
||||||
|
|
||||||
|
## RocketMQ 如何实现分布式事务?
|
||||||
|
|
||||||
|
如何解释分布式事务呢?事务大家都知道吧?**要么都执行要么都不执行** 。在同一个系统中我们可以轻松地实现事务,但是在分布式架构中,我们有很多服务是部署在不同系统之间的,而不同服务之间又需要进行调用。比如此时我下订单然后增加积分,如果保证不了分布式事务的话,就会出现 A 系统下了订单,但是 B 系统增加积分失败或者 A 系统没有下订单,B 系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。
|
||||||
|
|
||||||
|
那么,如何去解决这个问题呢?
|
||||||
|
|
||||||
|
如今比较常见的分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,**都不是完美的解决方案**。
|
||||||
|
|
||||||
|
在 `RocketMQ` 中使用的是 **事务消息加上事务反查机制** 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在第一步发送的 half 消息 ,它的意思是 **在事务提交之前,对于消费者来说,这个消息是不可见的** 。
|
||||||
|
|
||||||
|
> 那么,如何做到写入消息但是对用户不可见呢?RocketMQ 事务消息的做法是:如果消息是 half 消息,将备份原消息的主题与消息消费队列,然后 **改变主题** 为 RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费 half 类型的消息,**然后 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费**,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。
|
||||||
|
|
||||||
|
你可以试想一下,如果没有从第 5 步开始的 **事务反查机制** ,如果出现网路波动第 4 步没有发送成功,这样就会产生 MQ 不知道是不是需要给消费者消费的问题,他就像一个无头苍蝇一样。在 `RocketMQ` 中就是使用的上述的事务反查来解决的,而在 `Kafka` 中通常是直接抛出一个异常让用户来自行解决。
|
||||||
|
|
||||||
|
你还需要注意的是,在 `MQ Server` 指向系统 B 的操作已经和系统 A 不相关了,也就是说在消息队列中的分布式事务是——**本地事务和存储消息到消息队列才是同一个事务**。这样也就产生了事务的**最终一致性**,因为整个过程是异步的,**每个系统只要保证它自己那一部分的事务就行了**。
|
||||||
|
|
||||||
|
## 如何解决消息堆积问题?
|
||||||
|
|
||||||
|
在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢?
|
||||||
|
|
||||||
|
其实这个问题可以将它广义化,因为产生消息堆积的根源其实就只有两个——生产者生产太快或者消费者消费太慢。
|
||||||
|
|
||||||
|
我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 **限流降级** 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 **是否是消费者出现了大量的消费错误** ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
|
||||||
|
|
||||||
|
> 当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 **同时你还需要增加每个主题的队列数量** 。
|
||||||
|
>
|
||||||
|
> 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 什么事回溯消费?
|
||||||
|
|
||||||
|
回溯消费是指 `Consumer` 已经消费成功的消息,由于业务上需求需要重新消费,在`RocketMQ` 中, `Broker` 在向`Consumer` 投递成功消息后,**消息仍然需要保留** 。并且重新消费一般是按照时间维度,例如由于 `Consumer` 系统故障,恢复后需要重新消费 1 小时前的数据,那么 `Broker` 要提供一种机制,可以按照时间维度来回退消费进度。`RocketMQ` 支持按照时间回溯消费,时间维度精确到毫秒。
|
||||||
|
|
||||||
|
这是官方文档的解释,我直接照搬过来就当科普了 😁😁😁。
|
||||||
|
|
||||||
|
## RocketMQ 的刷盘机制
|
||||||
|
|
||||||
|
上面我讲了那么多的 `RocketMQ` 的架构和设计原理,你有没有好奇
|
||||||
|
|
||||||
|
在 `Topic` 中的 **队列是以什么样的形式存在的?**
|
||||||
|
|
||||||
|
**队列中的消息又是如何进行存储持久化的呢?**
|
||||||
|
|
||||||
|
我在上文中提到的 **同步刷盘** 和 **异步刷盘** 又是什么呢?它们会给持久化带来什么样的影响呢?
|
||||||
|
|
||||||
|
下面我将给你们一一解释。
|
||||||
|
|
||||||
|
### 同步刷盘和异步刷盘
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如上图所示,在同步刷盘中需要等待一个刷盘成功的 `ACK` ,同步刷盘对 `MQ` 消息可靠性来说是一种不错的保障,但是 **性能上会有较大影响** ,一般地适用于金融等特定业务场景。
|
||||||
|
|
||||||
|
而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行, **降低了读写延迟** ,提高了 `MQ` 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
|
||||||
|
|
||||||
|
一般地,**异步刷盘只有在 `Broker` 意外宕机的时候会丢失部分数据**,你可以设置 `Broker` 的参数 `FlushDiskType` 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
|
||||||
|
|
||||||
|
### 同步复制和异步复制
|
||||||
|
|
||||||
|
上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 `Borker` 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。
|
||||||
|
|
||||||
|
- 同步复制: 也叫 “同步双写”,也就是说,**只有消息同步双写到主从节点上时才返回写入成功** 。
|
||||||
|
- 异步复制: **消息写入主节点之后就直接返回写入成功** 。
|
||||||
|
|
||||||
|
然而,很多事情是没有完美的方案的,就比如我们进行消息写入的节点越多就更能保证消息的可靠性,但是随之的性能也会下降,所以需要程序员根据特定业务场景去选择适应的主从复制方案。
|
||||||
|
|
||||||
|
那么,**异步复制会不会也像异步刷盘那样影响消息的可靠性呢?**
|
||||||
|
|
||||||
|
答案是不会的,因为两者就是不同的概念,对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了 **可用性** 。为什么呢?其主要原因**是 `RocketMQ` 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了**。
|
||||||
|
|
||||||
|
比如这个时候采用异步复制的方式,在主节点还未发送完需要同步的消息的时候主节点挂掉了,这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了,**消费者可以自动切换到从节点进行消费**(仅仅是消费),所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况,降低了可用性,而当主节点重启之后,从节点那部分未来得及复制的消息还会继续复制。
|
||||||
|
|
||||||
|
在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?**一个主从不行那就多个主从的呗**,别忘了在我们最初的架构图中,每个 `Topic` 是分布在不同 `Broker` 中的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
但是这种复制方式同样也会带来一个问题,那就是无法保证 **严格顺序** 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 `Topic` 下的队列来保证顺序性的。如果此时我们主节点 A 负责的是订单 A 的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点 A 的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了。
|
||||||
|
|
||||||
|
而在 `RocketMQ` 中采用了 `Dledger` 解决这个问题。他要求在写入消息的时候,要求**至少消息复制到半数以上的节点之后**,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解。
|
||||||
|
|
||||||
|
> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制半数以上节点的效率和直接异步复制还是有一定的差距的。
|
||||||
|
|
||||||
|
### 存储机制
|
||||||
|
|
||||||
|
还记得上面我们一开始的三个问题吗?到这里第三个问题已经解决了。
|
||||||
|
|
||||||
|
但是,在 `Topic` 中的 **队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢?** 还未解决,其实这里涉及到了 `RocketMQ` 是如何设计它的存储结构了。我首先想大家介绍 `RocketMQ` 消息存储架构中的三大角色——`CommitLog` 、`ConsumeQueue` 和 `IndexFile` 。
|
||||||
|
|
||||||
|
- `CommitLog`: **消息主体以及元数据的存储主体**,存储 `Producer` 端写入的消息主体内容,消息内容不是定长的。单个文件大小默认 1G ,文件名长度为 20 位,左边补零,剩余为起始偏移量,比如 00000000000000000000 代表了第一个文件,起始偏移量为 0,文件大小为 1G=1073741824;当第一个文件写满了,第二个文件为 00000000001073741824,起始偏移量为 1073741824,以此类推。消息主要是**顺序写入日志文件**,当文件满了,写入下一个文件。
|
||||||
|
- `ConsumeQueue`: 消息消费队列,**引入的目的主要是提高消息消费的性能**(我们再前面也讲了),由于`RocketMQ` 是基于主题 `Topic` 的订阅模式,消息消费是针对主题进行的,如果要遍历 `commitlog` 文件中根据 `Topic` 检索消息是非常低效的。`Consumer` 即可根据 `ConsumeQueue` 来查找待消费的消息。其中,`ConsumeQueue`(逻辑消费队列)**作为消费消息的索引**,保存了指定 `Topic` 下的队列消息在 `CommitLog` 中的**起始物理偏移量 `offset` **,消息大小 `size` 和消息 `Tag` 的 `HashCode` 值。**`consumequeue` 文件可以看成是基于 `topic` 的 `commitlog` 索引文件**,故 `consumequeue` 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样 `consumequeue` 文件采取定长设计,每一个条目共 20 个字节,分别为 8 字节的 `commitlog` 物理偏移量、4 字节的消息长度、8 字节 tag `hashcode`,单个文件由 30W 个条目组成,可以像数组一样随机访问每一个条目,每个 `ConsumeQueue`文件大小约 5.72M;
|
||||||
|
- `IndexFile`: `IndexFile`(索引文件)提供了一种可以通过 key 或时间区间来查询消息的方法。这里只做科普不做详细介绍。
|
||||||
|
|
||||||
|
总结来说,整个消息存储的结构,最主要的就是 `CommitLoq` 和 `ConsumeQueue` 。而 `ConsumeQueue` 你可以大概理解为 `Topic` 中的队列。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RockeMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。
|
||||||
|
|
||||||
|
而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。
|
||||||
|
|
||||||
|
所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号\*索引固定⻓度 20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。
|
||||||
|
|
||||||
|
讲到这里,你可能对 `RockeMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。
|
||||||
|
|
||||||
|
> 如果上面没看懂的读者一定要认真看下面的流程分析!
|
||||||
|
|
||||||
|
首先,在最上面的那一块就是我刚刚讲的你现在可以直接 **把 `ConsumerQueue` 理解为 `Queue`**。
|
||||||
|
|
||||||
|
在图中最左边说明了红色方块代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic` 、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog**。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和 tag 的 hash 值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。
|
||||||
|
|
||||||
|
上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。
|
||||||
|
|
||||||
|
因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考 🤔🤔 一下吧。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
为什么 `CommitLog` 文件要设计成固定大小的长度呢?提醒:**内存映射机制**。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
总算把这篇博客写完了。我讲的你们还记得吗 😅?
|
||||||
|
|
||||||
|
这篇文章中我主要想大家介绍了
|
||||||
|
|
||||||
|
1. 消息队列出现的原因
|
||||||
|
2. 消息队列的作用(异步,解耦,削峰)
|
||||||
|
3. 消息队列带来的一系列问题(消息堆积、重复消费、顺序消费、分布式事务等等)
|
||||||
|
4. 消息队列的两种消息模型——队列和主题模式
|
||||||
|
5. 分析了 `RocketMQ` 的技术架构(`NameServer` 、`Broker` 、`Producer` 、`Comsumer`)
|
||||||
|
6. 结合 `RocketMQ` 回答了消息队列副作用的解决方案
|
||||||
|
7. 介绍了 `RocketMQ` 的存储机制和刷盘策略。
|
||||||
|
|
||||||
|
等等。。。
|
||||||
|
@ -375,9 +375,9 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
|
|||||||
|
|
||||||
消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: [消息队列常见问题总结](./high-performance/message-queue/message-queue.md)。
|
消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: [消息队列常见问题总结](./high-performance/message-queue/message-queue.md)。
|
||||||
|
|
||||||
- **RabbitMQ** : [RabbitMQ 基础知识总结](./high-performance/message-queue/rabbitmq-intro.md)、[RabbitMQ 常见面试题](./high-performance/message-queue/rabbitmq-questions.md)
|
- [RabbitMQ 常见面试题](./high-performance/message-queue/rabbitmq-questions.md)
|
||||||
- **RocketMQ** : [RocketMQ 基础知识总结](./high-performance/message-queue/rocketmq-intro.md)、[RocketMQ 常见面试题总结](./high-performance/message-queue/rocketmq-questions.md)
|
- [RocketMQ 常见面试题总结](./high-performance/message-queue/rocketmq-questions.md)
|
||||||
- **Kafka** :[Kafka 常见问题总结](./high-performance/message-queue/kafka-questions-01.md)
|
- [Kafka 常见问题总结](./high-performance/message-queue/kafka-questions-01.md)
|
||||||
|
|
||||||
## 高可用
|
## 高可用
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ star: 5
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
每一篇内容都非常干货,不少球友看了之后表示收获漫漫。不过,最重要的还是知行合一。
|
每一篇内容都非常干货,不少球友看了之后表示收获满满。不过,最重要的还是知行合一。
|
||||||
|
|
||||||
## 星球其他资源
|
## 星球其他资源
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user