[29] RabbitMQ-QuickStart
1. 概述#
RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol)基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
RabbitMQ 是使用 Erlang 编写的一个开源的消息队列(消息队列就是一个使用队列来通信的组件),本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。它同时实现了一个 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队,对路由(Routing)、负载均衡(Load balance)或者数据持久化都有很好的支持。
MQ 对比:
MQ 的特点:
- 可靠性:RabbitMQ 使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。
- 灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。
- 扩展性:多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。
- 高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。
- 多种协议:RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP, MQTT 等多种消息 中间件协议。
- 多语言客户端:RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。
- 管理界面:RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。
- 插件机制:RabbitMQ 提供了许多插件 ,以实现从多方面进行扩展,当然也可以编写自己的插件。
2. 核心概念#
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。
2.1 生产者/消费者#
生产者 :
- 消息生产者,就是投递消息的一方。
- 消息一般包含两个部分:消息体(payload)和标签(Label)。
消费者:
- 消费消息,也就是接收消息的一方。
- 消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
消息:
- 消息一般由 2 部分组成:消息头(或者说是标签 Label)和消息体。
- 消息体也可以称为 payLoad,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
- 生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的消费者。
2.2 交换机#
在 RabbitMQ 中,消息并不是直接被投递到 Queue(消息队列)中的,中间还必须经过 Exchange(交换器)这一层,Exchange(交换器)会把我们的消息分配到对应的 Queue(消息队列)中。
Exchange 用来接收 Producer 发送的消息并将这些消息路由给服务器中的 Queue 中,如果路由不到,或许会返回给 Producer,或许会被直接丢弃掉。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
生产者将消息发给交换器的时候,一般会指定一个 RoutingKey(路由键),用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
RabbitMQ 中通过 Binding(绑定)将 Exchange 与 Queue 关联起来,在绑定的时候一般会指定一个 BindingKey(绑定键) ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。
一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 Fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
注:如果不指定交换机而直接发送路由键和消息,RabbitMQ会使用默认的直连交换机进行路由。默认交换机隐式地绑定到每个队列,其路由键等于队列名称。不可能显式绑定到默认交换机,也不可能从默认交换机解除绑定,也不能删除。
2.3 队列#
Queue(消息队列)用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
RabbitMQ 中消息只能存储在 Queue 中,这一点和 Kafka 这种消息中间件相反。Kafka 将消息存储在 topic(主题)这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
注:RabbitMQ 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
2.4 服务器#
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整个流程。
2.5 连接#
- Connection:生产者或消费者与 Broker 建立的 TCP 连接。
- Channel:一旦 TCP 连接建立起来,客户端就可以创建一个 AMQP 信道(Channel),每个信道都会被指派一个唯一的 ID,信道是建立在 Connection 之上的虚拟连接,RabbitMQ 处理每条 AMQP 指令都是通过信道完成的。
2.6 名词小结#
名词 | 说明 |
---|---|
Broker | 接收和分发消息的应用,RabbitMQ Server 就是 Message Broker。 |
Virtual Host | 出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ Server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 Exchange/Queue 等。 |
Connection | Producer/Consumer 和 broker 之间的 TCP 连接。 |
Channel | 如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 Connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 Thread 创建单独的 Channel 进行通讯,AMQP method 包含了ChannelID 帮助客户端和 Message Broker 识别 Channel,所以 Channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销。 |
Exchange | Message 到达 Broker 的第一站,根据分发规则,匹配查询表中的 Routing Key,分发消息到 Queue 中去。常用的类型有:Direct (point-to-point)、Topic (publish-subscribe) and Fanout (multicast)。 |
Queue | 消息最终被送到这里等待 Consumer 取走。 |
Binding | exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据。 |
3. Exchange 类型#
RabbitMQ 的 Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略:direct、fanout、topic、headers,不同类型的 Exchange 转发消息的策略有所区别。
什么是 bingding 呢?binding 其实是 Exchange 和 Queue 之间的桥梁,它告诉我们 Exchange 和哪个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定。
3.1 Fanout#
fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
系统中默认有些 Exchange 类型。
3.2 Direct#
Direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 BindingKey 与 RoutingKey 完全匹配的 Queue 中。
- P:生产者,向 Exchange 发送消息,发送消息时会指定一个 Routing Key
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 Binding Key 完全匹配的队列
- C1:消费者,其所在队列指定了需要 Routing Key 为 error 的消息
- C2:消费者,其所在队列指定了需要 Routing Key 为 info、error、warning 的消息
Direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。
如果 Exchange 的绑定类型是 Direct,但是它绑定的多个队列的 BindingKey 如果都相同,在这种情况下虽然绑定类型是 Direct 但是它表现的就和 Fanout 有点类似了,就跟广播差不多,如图所示。
3.3 Topics#
前面讲到 Direct 类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。
topic 类型的交换器在匹配规则上进行了扩展,它与 Direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:Topic 类型 Exchange 可以让队列在绑定 RoutingKey 的时候使用通配符!
- RoutingKey 为一个点号
.
分隔的字符串(被点号.
分隔开的每一段独立的字符串称为一个单词),如 "com.rabbitmq.client"、"java.util.concurrent"、"com.hidden.client",这个单词列表最多不能超过 255 个字节。 - BindingKey 和 RoutingKey 一样也是点号
.
分隔的字符串。 - BindingKey 中可以存在两种特殊字符串
*
和#
,用于做模糊匹配,其中*
用于匹配一个单词,#
用于匹配多个单词(可以是 0 个)。
示例:
- 红色 Queue:绑定的是
usa.#
,因此凡是以usa.
开头的 routing key 都会被匹配; - 黄色 Queue:绑定的是
#.news
,因此凡是以.news
结尾的 routing key 都会被匹配;
当队列绑定关系是下列这种情况时需要引起注意:
- 当一个队列绑定键是 #,那么这个队列将接收所有数据,就有点像 fanout 了。
- 如果队列绑定键当中没有 # 和 * 出现,那么该队列绑定类型就是 direct 了。
3.4 Headers#
Headers 类型的 Exchange 不依赖于路由键的匹配规则来路由信息,而是根据发送的消息内容中的 Headers(消息头)属性进行匹配。
在绑定队列和交换器时指定一组键值对,当发送的消息到 Exchange 时,RabbitMQ 会获取到该消息的 Headers(也是一个键值对的形式),对比其中的键值对是否完全匹配 Queue 和 Exchange 绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
Headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
4. 存储机制#
4.1 消息存储#
「持久层」是一个逻辑上的概念,包含了两部分:rabbit_queue_index(队列索引)和 rabbit_msg_store(消息存储)。
- 消息本身,由 msg_store 模块负责 [$RABBIT_SRC/src/rabbit_msg_store.erl]
- rabbit_msg _store 以键值对的形式存储消息,整个 node 只有一个,被所有的 Queue 共享。又可以细分为 msg_store_persistent 和 msg_store_transient。
- msg_store_persistent 是负责持久化消息的存储,node 重启消息不会丢失;msg_store_transient 是负责非持久化消息的存储,node 重启后消息丢失。
- 消息在队列中的位置,由 queue_index 模块负责 [$RABBIT_SRC/src/rabbit_queue_index.erl]
- rabbit_queue_index(队列索引)负责维护存储的消息的信息,包含了存储地址、是否已经交付给消费者、消费者是否已经 ack 等,职责有点像 MySQL 的索引,但是多了消息状态的维护。
- 每一个 Queue 都会有与之对应的一个 rabbit_queue_index。
a. rabbit_queue_index#
rabbit_queue_index 以顺序(文件名从 0 开始累加)的段文件来进行数据存储,后缀为“.idx”。大致效果如下:
- xxx0.idx
- xxx1.idx
- xxx2.idx
每个段文件固定存储 SEGMENT_ENTRY_COUNT 条记录,默认值为 16384。rabbit_queue_index 要从磁盘读取数据的时候,至少要在内存维护一个段文件,也就是说,需要把整个段文件加载到内存,再读取段里面的内容。
由于这样的机制,调整 SEGMENT_ENTRY_COUNT 和 queue_index_embed_msgs_below 两个的值需要注意,一点点增大也很容易引发内存的暴增:
- SEGMENT_ENTRY_COUNT 翻倍,每个段大小翻倍
- queue_index_embed_msgs_below 翻倍,每个段大小也翻倍。
Q1:消息都存储在 rabbit_msg_store?
消息不是一定需要存储在 rabbit_msg_store,MQ 也支持直接把消息存储在 rabbit_queue_index。
最佳的方式是较小的消息直接存储在 rabbit_queue_index,较大的消息存储在 rabbit_msg _store。消息应该放在 rabbit_queue_index 还是 rabbit_msg _store 是根据 queue_index_embed_msgs_below 配置来决定的,默认是 4096B,当消息的 header、body 和 property 总大小小于该配置,则消息直接在 index 中存储,如果大于这个值就在 store 中存储。
Q2:为什么要将较小的消息存储在 rabbit_queue_index?
目的是提高性能!正常查找一个消息,是需要在 rabbit_queue_index 找到消息的存储位置,再去 rabbit_msg _store 里找到消息,需要一次中转,把消息直接放到 rabbit_queue_index中,可以节省这一步。
b. rabbit_msg_store#
rabbit_msg_store 存储消息:
消息以键值对的形式存储到文件中,一个虚拟主机上的所有队列使用同一块存储,每个节点只有一个。存储分为持久化存储(msg_store_persistent)和短暂存储(msg_store_transient)。持久化存储的内容在 broker 重启后不会丢失,短暂存储的内容在 broker 重启后丢失。
由 rabbit_msg_store 负责存储的消息,都是以追加的方式写入到文件中,当文件大小超过 file_size_limit 之后,就会关闭这个文件,创建一个新的文件给新消息写入。文件以“.rdq”结尾,从 0 开始累加。在进行消息的存储时,RabbitMQ 会在 ETS(Erlang Term Storage)表中记录消息在文件中的位置映射和文件的相关信息。
- xxx0.rdq
- xxx1.rdq
- xxx2.rdq
rabbit_msg_store 读取消息:
RabbitMQ 在 ETS(Erlang Term Storage)表中记录消息在哪个 rdq 文件,在文件中的哪个位置。
读取消息的时候,先根据消息 ID,从 ETS 中找到存储的信息,并找到对应的文件,如果文件存在并且没有被锁住,则直接打开文件,从指定位置读取消息的内容。如果文件不存在或者被锁住,则交给 rabbit_msg_store 处理。
rabbit_msg_store 删除消息:
删除消息时,只是从 ETS 表删除指定消息的相关信息,同时更新消息对应的存储文件和相关信息。
在执行消息删除操作时,并不立即对文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而已。当一个文件中都是垃圾数据时可以将这个文件删除。当检测到前后两个文件中的有效数据可以合并成一个文件,并且所有的垃圾数据的大小和所有文件(至少有 3 个文件存在的情况下)的数据大小的比值超过设置的阈值 garbage_fraction(默认值 0.5)时,才会触发垃圾回收,将这两个文件合并,执行合并的两个文件一定是逻辑上相邻的两个文件。
合并逻辑:
- 锁定这两个文件
- 先整理前面的文件的有效数据,再整理后面的文件的有效数据
- 将后面文件的有效数据写入到前面的文件中
- 更新消息在 ETS 表中的记录
- 删除后面文件
4.2 消息状态#
RabbitMQ 中的消息一旦进入队列,不是固定不变的,它会随着系统的负载在队列中不断流动,消息的不断发生变化。
rabbit_variable_queue.erl 源码中定义了消息的 4 种状态:
消息状态 | 存储说明 | 资源消耗情况 |
---|---|---|
alpha | 消息内容(包括消息体、属性和 headers)和消息索引都存储在 RAM 中。 | 最消耗内存,很少消耗 CPU,不需要磁盘 IO。 |
beta | 消息内容保存在 DISK 中,消息索引保存在 RAM 中。 | 消耗的内存不多,只有索引在内存中,需要一次磁盘 IO。 |
gramma | 消息内容保存在 DISK,消息索引在 RAM 和 DISK 中都有。 | 消耗的内存不多,只有索引在内存中,需要一次磁盘 IO。 |
delta | 消息内容和索引都在 DISK 中。 | 基本不消耗内存,但是需要消耗最多的 CPU 和两次磁盘 IO,一次从磁盘读索引,一次读消息内容。 |
对于持久化的消息,消息内容和消息索引都必须保存在磁盘上,才会处于上述状态中的一种。而 gamma 状态的消息是只有持久化的消息才会有的。这个结论要成立,需要满足:非持久化的消息,内存不足也不会把索引持久化到磁盘。
4.3 队列结构#
通常队列由 rabbit_amqqueue_process 和 backing_queue 这两部分组成。
队列组成 | AMQQueue | BackingQueue |
---|---|---|
erl | rabbit_amqqueue_process | backing_queue |
说明 | 负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的 confirm 和消费端的 ack)等。 | 是消息存储的具体形式和引擎,并向 rabbit_amqqueue_process 提供相关的接口以供调用。BackingQueue 又由 5 个子队列组成:Q1, Q2, Delta, Q3和Q4。 |
对于普通的没有设置优先级和镜像的队列来说,backing_queue 的默认实现是 rabbit_variable_queue,其内部通过 5 个子队列 Q1、Q2、delta、Q3、Q4 来体现消息的各个状态:
子队列 | 存储类型 |
---|---|
Q1 | 只存储 alpha 状态的消息 |
Q2 | 存储 beta 和 gramma 状态的消息 |
Delta | 只存储 delta 状态的消息 |
Q3 | 存储 beta 和 gramma 状态的消息 |
Q4 | 只存储 alpha 状态的消息 |
进入队列的消息,一般会按照 Q1-> Q2 -> Delta -> Q3 -> Q4 的顺序流转,但不是每个消息都必须经历所有的状态,这个取决当前系统的负载,系统负载低的时候,可能Q1 -> Q4,不需要经过 Q2、Delta、Q3。
引起消息流动主要有两方面因素:
- 消费者获取消息
- 由于内存不足引起消息换出到磁盘
(1)由于内存不足引起消息换出到磁盘
在运行时,RabbitMQ 会根据消息传递速度定期计算出一个当前内存能够保存的最大消息数量(target_ram_count)。如果 alpha 状态的消息数量大于这个值,则会引起消息的状态转换,多余的消息可能会转换到 beta 状态、gramma 状态或 delta 状态。区分这 4 种状态的主要作用是满足不同的内存和 CPU 需求。
(2)消费引起的消息流动
消费者获取消息也会引起消息的状态转换,当消费者获取消息时:
- 从 Q4 获取消息
- 如果 Q4(内部缓冲区)中有消息,消费者将首先从 Q4 中获取消息。这是为了提高性能,因为 Q4 中的消息已经加载到内存中,可以直接返回给消费者。
- 从 Q3 获取消息 // 如果 Q4 为空,系统会检查 Q3(恢复队列)
- 如果 Q3 中有消息,消费者将从 Q3 中获取消息。
- 如果 Q3 为空,则返回队列为空的信息,表明当前队列中没有可用的消息。
- 从 Q1 转移消息到 Q4
- 如果 Q3 为空,且 Delta(重发队列)为空,这表明 Q2、Delta、Q3、Q4 全部为空。
- 此时,系统会将 Q1(就绪队列)中的消息转移到 Q4 中,以便下次直接从 Q4 中获取消息。
- 从 Delta 转移消息到 Q3
- 如果 Q3 为空,但 Delta 不为空,系统会将 Delta 中的消息转移到 Q3 中,以便下次直接从 Q3 中获取消息。这个转移过程是按索引分段进行的,即系统会先读取 Delta 中的一段消息。
- 判断读取的消息数量是否等于 Delta 中剩余的消息数量:
- 如果相等,说明 Delta 中已无消息,此时可以将 Q2 中的消息以及刚读取到的消息一起放入 Q3 中。
- 如果不相等,说明 Delta 中还有消息未读取,此时仅将此次读取到的消息转移到 Q3。
为什么 Q3 为空则可以认定整个队列为空?
- 试想一下,如果 Q3 为空,Delta 不为空,那么在 Q3 取出最后一条消息的时候,Delta 上的消息就会被转移到 Q3,这样与 Q3 为空矛盾;
- 如果 Delta 为空且 Q2 不为空,则在 Q3 取出最后一条消息时会将 Q2 的消息并入到 Q3 中,这样也与 Q3 为空矛盾;
- 在 Q3 取出最后一条消息之后,如果 Q2、Delta、Q3 都为空,且 Q1 不为空时,则 Q1 的消息会被转移到 Q4,这与 Q4 为空矛盾。
其实这一番论述也解释了另一个问题:为什么 Q3 和 Delta 都为空时,则可以认为 Q2、Delta、Q3、Q4 全部为空?
- 通常在负载正常时,如果消费速度大于生产速度,对于不需要保证可靠不丢失的消息来说,极有可能只会处于 alpha 状态。
- 对于持久化消息,它一定会进入 gamma 状态,在开启 Publisher Confirm 机制时,只有到了 gamma 状态时才会确认该消息已被接收,若消息消费速度足够快、内存也充足,这些消息也不会继续走到下一个状态。
为什么消息的堆积导致性能下降?
在系统负载较高时,消息若不能很快被消费掉,这些消息就会进入到很深的队列中去,这样会增加处理每个消息的平均开销。因为要花更多的时间和资源处理“堆积”的消息,如此用来处理新流入的消息的能力就会降低,使得后流入的消息又被积压到很深的队列中,继续增大处理每个消息的平均开销,继而情况变得越来越恶化,使得系统的处理能力大大降低。
应对这一问题一般有 3 种措施:
- 增加 prefetch_count 的值,即一次发送多条消息给消费者,加快消息被消费的速度。
- 采用 multiple ack,降低处理 ack 带来的开销
- 流量控制
5. 安装配置#
安装环境:
RabbitMQ 的安装需要首先安装 Erlang,因为它是基于 Erlang 的 VM 运行的。
RabbitMQ 需要的依赖:socat 和 logrotate,logrotate 操作系统中已经存在了,只需要安装 socat 就可以了。
RabbitMQ 与 Erlang 的兼容关系详见:https://www.rabbitmq.com/which-erlang.html
5.1 安装步骤#
ARM 架构的 CentOS 虚拟机下进行 RabbitMQ 安装
* YUM#
YUM 坏了,网上找了好久解决办法,解决办法都是换个镜像源。我试了没用,到头来突然想起,我这是 Mac 上装的 CentOS 虚拟机!网上提供的办法都是 Window 上装的虚拟机的。-_-''
查看当前 CentOS 版本:
[root@centos7 ~]# cat /etc/centos-release
CentOS Linux release 7.9.2009 (AltArch)
注意看!版本是 CentOS AltArch 不是 CentOS!
CentOS 是一个基于 Linux 的开源操作系统,常用于服务器和企业计算环境。"altarch" 通常指的是非 x86 架构的计算机,例如 ARM 架构。在中国,由于网络问题,直接使用官方源可能会较慢或不可用,因此需要使用国内的镜像源。
以下是一个示例,展示如何为 CentOS 的 ARM 架构(假设您使用的是基于 ARM 的设备,如 Raspberry Pi 或其他 ARM 系统)设置国内源。
- 首先备份你的 CentOS ARM 源配置文件:
sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
- 编辑 CentOS-Base.repo 文件:
sudo vi /etc/yum.repos.d/CentOS-Base.repo
- 替换文件内容为国内的 CentOS ARM 镜像源。例如,使用阿里云的镜像源。
[base] name=CentOS-$releasever - Base - Aliyun baseurl=http://mirrors.aliyun.com/centos-altarch/$releasever/os/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos-altarch/RPM-GPG-KEY-CentOS-7 #released updates [updates] name=CentOS-$releasever - Updates - Aliyun baseurl=http://mirrors.aliyun.com/centos-altarch/$releasever/updates/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos-altarch/RPM-GPG-KEY-CentOS-7 #additional packages that may be useful [extras] name=CentOS-$releasever - Extras - Aliyun baseurl=http://mirrors.aliyun.com/centos-altarch/$releasever/extras/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos-altarch/RPM-GPG-KEY-CentOS-7
a. Erlang#
Erlang 是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信所辖的 CS-Lab 开发,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。
最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式、实时软并行计算系统。Erlang 运行时环境是一个虚拟机,有点像 Java 的虚拟机,这样代码一经编译,同样可以随处运行。
RabbitMQ 安装包依赖于 Erlang 语言包的支持,所以需要先安装 Erlang 语言包,再安装 RabbitMQ 安装包。下载 Erlang 时需要注意版本兼容性问题,参考官方文档(https://www.rabbitmq.com/which-erlang.html)。
ARM 架构的 CentOS 虚拟机中安装 Erlang
(1)下载 ARM 架构版本的 Erlang 安装包。可以从以下三个地址下载 23.3.4.14 版本的安装包。
- https://github.com/erlang/otp/releases
- https://www.erlang-solutions.com/downloads
- https://erlang.org/download/otp_versions_tree.html
(2)拷贝 otp-OTP-23.3.4.14.tar.gz 到 /opt/software 目录
$ scp /Users/tree6x7/Downloads/otp-OTP-23.3.4.14.tar.gz root@192.168.6.160:/opt/software
(3)安装依赖环境
$ yum -y install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz glibc-devel xmlto perl gtk2-devel binutils-devel
(4)解压 Erlang 安装包
$ tar -zxvf otp-OTP-23.3.4.14.tar.gz
(5)创建并设置安装目录
# 创建安装目录
$ mkdir /opt/module/erlang
# 进入Erlang解压后的源码目录
$ cd otp-OTP-23.3.4.14
# 设置安装目录
$ ./configure --prefix=/opt/erlang
(6)编译、安装 Erlang
$ make
$ make install
(7)配置 ERLANG_HOME 环境变量
已知 /etc/profile 可以看到会去加载 /etc/profile.d 下的所有脚本,所以为了方便管理,所有自定义的环境变量均放在 /etc/profile.d/my_env.sh 下:
# ERLANG_HOME
export ERLANG_HOME=/opt/module/erlang
export PATH=$PATH:$ERLANG_HOME/bin
(8)使配置文件生效并使用 erl 命令查看 Erlang 的版本信息
# 使环境变量生效
$ source /etc/profile
# 查看当前Erlang版本
$ erl -version
Erlang (SMP,ASYNC_THREADS) (BEAM) emulator version 11.2.2.13
b. Socat#
Socat 是 Linux 下的一个多功能的网络工具,名字来由是“Socket CAT”。其功能与有瑞士军刀之称的 Netcat 类似,可以看做是 Netcat 的加强版。
Socat 的主要特点就是在两个数据流之间建立通道,且支持众多协议和链接方式。如 IP、TCP、UDP、IPv6、PIPE、EXEC、System、Open、Proxy、Openssl、Socket 等。Socat 的官方网站:http://www.dest-unreach.org/socat。
ARM 架构的 CentOS 虚拟机中在安装 Erlang 时,默认已经安装了 Socat,因此无需重复安装 Socat。而 X86 架构的 CentOS 虚拟机中在安装 Erlang 时,默认没有安装 Socat,因此需要手动安装 Socat:yum install socat -y
c. RabbitMQ#
下载 ARM 架构版本的 RabbitMQ 安装包,在 RabbitMQ 发布的所有版本中,支持 CentOS 7.X 的最高一个版本是 rabbitmq-server-3.10.0。
(1)解压 RabbitMQ 安装包,并将解压目录也放到 /opt/module 下
$ xz -d rabbitmq-server-generic-unix-3.10.0.tar.xz
$ tar -xvf rabbitmq-server-generic-unix-3.10.0.tar
$ mv rabbitmq_server-3.10.0 ../module/
(2)配置 RabbitMQ 环境变量
# RABBITMQ_HOME
export RABBITMQ_HOME=/opt/module/rabbitmq_server-3.10.0
export PATH=$PATH:$RABBITMQ_HOME/sbin
(3)使环境变量生效
# 使环境变量生效
$ source /etc/profile
(4)RabbitMQ 目录结构
(5)启用 RabbitMQ 的管理插件以及 RabbitMQ 本 Q~
# 启动管理插件
$ rabbitmq-plugins enable rabbitmq_management
# 前台启动RabbitMQ
$ rabbitmq-server
# 后台启动RabbitMQ
$ rabbitmq-server -detached
5.2 首次配置#
(0)RabbitMQ 默认提供了一个 guest/guest 账户,登录 http://192.168.6.160:15672/ 管理台界面,试图登录:
(1)添加用户
# 添加用户 root
$ rabbitmqctl add_user root 123456
# 给root用户在虚拟主机"/"上的配置、写、读的权限
$ rabbitmqctl set_permissions root -p / ".*" ".*" ".*"
# 给用户设置标签
$ rabbitmqctl set_user_tags root administrator
用户的标签及含义:
效果如下:
打开浏览器,访问 http://192.168.6.160:15672/ 管理台界面,试图用 root/123456 登录:
5.3 常用命令#
# 查看该命令的帮助文档
man rabbitmq-server
# 前台启动Erlang VM和RabbitMQ
rabbitmq-server
# 后台启动
rabbitmq-server -detached
# 停止RabbitMQ和ErlangVM
rabbitmqctl stop
# 查看所有队列
rabbitmqctl list_queues
# 查看所有虚拟主机
rabbitmqctl list_vhosts
# 在ErlangVM运行的情况下启动RabbitMQ应用
rabbitmqctl start_app
rabbitmqctl stop_app
# 查看节点状态
rabbitmqctl status
# 查看所有可用的插件
rabbitmq-plugins list
# 启用插件
rabbitmq-plugins enable <plugin-name>
# 停用插件
rabbitmq-plugins disable <plugin-name>
# 创建虚拟主机
rabbitmqctl add_vhost vhostpath
# 列出所以虚拟主机
rabbitmqctl list_vhosts
# 列出虚拟主机上的所有权限
rabbitmqctl list_permissions -p vhostpath
# 删除虚拟主机
rabbitmqctl delete_vhost vhost vhostpath
# 添加用户
rabbitmqctl add_user username password
# 修改密码
rabbitmqctl change_password username newpassword
# 列出所有用户
rabbitmqctl list_users
# 删除用户
rabbitmqctl delete_user username
# 设置用户权限 [--vhost <vhost>] <username> <conf> <write> <read>
rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
rabbitmqctl set_permissions --vhost / username "^$" ".*" ".*"
# 列出用户权限
rabbitmqctl list_user_permissions username
# 清除用户权限
rabbitmqctl clear_permissions -p vhostpath username
# 移除所有数据,要在 rabbitmqctl stop_app 之后使用
rabbitmqctl reset
6. MQ 工作流程#
6.1 收发消息流程#
生产者发送消息的流程
- 生产者连接 RabbitMQ,建立 TCP 连接(Connection),开启信道(Channel)。
- 生产者声明一个 Exchange(交换器),并设置相关属性,比如交换器类型、是否持久化等。
- 生产者声明一个 Queue(队列)并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
- 生产者通过 bindingKey (绑定Key)将交换器和队列绑定(binding)起来。
- 生产者发送消息至 RabbitMQ Broker,其中包含 routingKey (路由键)、交换器等信息。
- 相应的交换器根据接收到的 routingKey 查找相匹配的队列。
- 如果找到,则将从生产者发送过来的消息存入相应的队列中。
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。
- 关闭信道。
- 关闭连接。
消费者接收消息的过程
- 消费者连接到 RabbitMQ Broker ,建立一个连接(Connection),开启一个信道(Channel) 。
- 消费者向 RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数, 以及做一些准备工作。
- 等待 RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。
- 消费者确认(ack)接收到的消息。
- RabbitMQ 从队列中删除相应己经被确认的消息。
- 关闭信道。
- 关闭连接。
6.2 连接&信道#
Connection 和 Channel 关系
生产者和消费者,需要与RabbitMQ Broker 建立 TCP 连接,也就是 Connection 。一旦 TCP 连接建立起来,客户端紧接着创建一个 AMQP 信道(Channel),每个信道都会被指派一个唯一的 ID。信道是建立在 Connection 之上的虚拟连接, RabbitMQ 处理的每条 AMQP 指令都是通过信道完成的。
为什么不直接使用 TCP 连接,而是使用信道?
复用 TCP 连接,减少性能开销,便于管理。当每个信道的流量不是很大时,复用单一的 Connection 可以在产生性能瓶颈的情况下有效地节省 TCP 连接资源。
当信道本身的流量很大时,一个 Connection 就会产生性能瓶颈,流量被限制。需要建立多个 Connection,分摊信道。具体的调优看业务需要。
信道在 AMQP 中是一个很重要的概念,大多数操作都是在信道这个层面进行的。
RabbitMQ 相关的 API 与 AMQP 紧密相连,比如 channel.basicPublish 对应 AMQP 的 Basic.Publish 命令。
6.3 HelloWorld#
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
a. HelloProducer#
public class HelloProducer {
public static void main(String[] args) {
// 连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置服务器主机名或IP地址
factory.setHost("192.168.6.160");
// 设置虚拟主机名称
factory.setVirtualHost("/");
// 设置用户名
factory.setUsername("root");
// 设置密码
factory.setPassword("123456");
// 设置客户端与服务器的通信端口,默认值为5672
factory.setPort(5672);
// 获取连接
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 从连接获取通道
// 声明一个队列
// p1 队列名称
// p2 false表示在rabbitmq-server重启后就没有了
// p3 表示该队列不是一个排外队列,否则一旦客户端断开,队列就删除了
// p4 表示该队列是否自动删除,true表示一旦不使用了,系统删除该队列
// p5 表示该队列的参数,该参数是Map集合,用于指定队列的属性
channel.queueDeclare("queue.biz", false, false, true, null);
// 声明交换器
// p1 交换器的名称
// p2 交换器的类型
// p3 交换器是否是持久化的
// p4 交换器是否是自动删除的
// p5 交换器的属性Map集合
channel.exchangeDeclare("ex.biz", BuiltinExchangeType.DIRECT, false, false, null);
// 将交换器和消息队列绑定,并指定路由键
channel.queueBind("queue.biz", "ex.biz", "hello.world");
// 发送消息
// 交换器的名字
// 该消息的路由键
// 该消息的属性BasicProperties对象
// 消息的字节数组
channel.basicPublish("ex.biz", "hello.world", null, "hello world 1".getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
b. HelloConsumeConsumer#
public class HelloConsumeConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 确保MQ中有该队列,如果没有则创建
channel.queueDeclare("queue.biz", false, false, true, null);
// 消息的推送回调函数
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
// 监听消息,一旦有消息推送过来,就调用第一个lambda表达式
channel.basicConsume("queue.biz", deliverCallback, consumerTag -> {
});
}
}
实际上,RabbitMQ 采用了一种混合的方式,称为发布/订阅模式的一部分,但更具体地说,这是通过消费者轮询(consumer polling)机制实现的。
当消费者想要接收消息时,它会向 RabbitMQ 发起一个订阅(subscription)请求。这通常通过创建一个消费者并与特定的队列建立关联来完成。一旦建立了这种关联,RabbitMQ 就会在内部维护一个到消费者的连接,并且在有新消息到达队列时,主动地将消息发送给消费者。
具体来说,当消费者连接到队列后,它会进入等待状态。此时,如果队列中有可用的消息,RabbitMQ 的服务器端会主动地将消息发送到客户端的回调函数中。这个过程看似是“推送”,但实际上,客户端仍然需要保持一个活动的网络连接以便接收消息。这种方式允许客户端在没有消息时休眠,从而节省资源。
在 AMQP 协议中,这被称为 Basic Consume 方法。当调用此方法时,可以指定一个回调函数,每当队列中有消息可供消费时,RabbitMQ 就会调用这个回调函数,并将消息传递给消费者。
总结一下,在 RabbitMQ 中,消费者通过注册回调函数来接收消息,这看起来像是 RabbitMQ 在“推送”消息,但实际上,它是通过维持一个长期的 TCP 连接,并在这个连接上发送消息给消费者端的应用程序来实现的。这种方式既不是传统的拉取模型,也不是纯粹的推送模型,而是结合了两者特点的一种机制。
c. HelloGetConsumer#
public class HelloGetConsumer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 指定协议 amqp://
// 指定用户名 root
// 指定密码 123456
// 指定host node1
// 指定端口号 5672
// 指定虚拟主机 '/'在url中的转义字符为%2f
factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 确保MQ中有该队列,如果没有则创建
channel.queueDeclare("queue.biz", false, false, true, null);
// 拉消息模式
// 指定从哪个消费者消费消息
// 指定是否自动确认消息 true表示自动确认
final GetResponse getResponse = channel.basicGet("queue.biz", true);
// 获取消息体 hello world 1
final byte[] body = getResponse.getBody();
System.out.println(new String(body));
}
}
}
在 RabbitMQ 中,basicConsume
和 basicGet
是两个用于从队列中获取消息的方法,它们之间有几个关键区别:
basicConsume | basicGet | |
---|---|---|
消息获取方式 | ① 这是一个异步方法,用于注册一个消费者来监听队列。当有消息到达队列时,RabbitMQ 会自动将这些消息推送给消费者。 ② 消费者需要实现一个回调函数,以处理接收到的消息。 |
① 这是一个同步方法,用于主动从队列中获取一条消息。它会立即返回一条消息(如果队列不为空),或者返回 null (如果队列为空)。 ② 适合用在你需要偶尔检查消息的场景,而不是持续监听。 |
性能 | 通常在高负载情况下更高效,因为它利用了 RabbitMQ 的推送机制,可以减少网络延迟和负担。 | 每次调用都需要与 RabbitMQ 服务器建立通信,可能导致性能开销较大,尤其是在高频率请求时。 |
应用场景 | 适合需要实时处理消息的应用,如消息推送、通知服务等。 | 适合低频率或需要手动控制消息处理的场景,如任务调度或后台处理。 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?