RabbitMQ基础理论
RabbitMQ整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。整体模型如下:

RabbitMQ相关概念
生产者和消费者
-
Producer:生产者,创建消息,然后发布到RabbitMQ中。消息一般可以包括2个部分:消息体和标签(Label)。消息体一般称为payload。实际中,消息体一般是一个带有业务逻辑结构的数据,比如一个JSON字符串。消息的标签用来表示这条消息,比如一个交换器的名称和一个路由键。生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)。
-
Consumer:消费者,接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体。
-
Broker:消息中间件的服务节点。对于RabbitMQ来说,一个RabbitMQ Broker可以简单地看作一个RabbitMQ服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个RabbitMQ Broker看作一台RabbitMQ服务器。
如下是生产者将消息存入RabbitMQ Broker,以及消费者从Broker中消费数据的流程图:

首先,生产者将业务方数据进行可能的包装,之后封装成消息,发送(AMQP协议里这个动作对应的命令为Basic.Publish)到Broker中。消费者订阅并接收消息(AMQP协议里这个动作对应的命令为Basic.Consume或者Basic. Get) ,经过可能的解包处理得到原始的数据,之后再进行业务处理逻辑。这个业务处理逻辑并不一定需要和接收消息的逻辑使用同一个线程。消费者进程可以使用一个线程去接收消息,存入到内存中,比如使用Java 中的BlockingQueue。业务处理逻辑使用另一个线程从内存中读取数据,这样可以将应用进一步解稿,提高整个应用的处理效率。
队列
- Queue:队列,即RabbitMQ的内部对象,用于存储消息。

队列属性
队列跟交换机共享某些属性,但是队列也有一些另外的属性。
- Name
- Durable(消息代理重启后,队列依旧存在)
- Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除)
- Auto-delete(当最后一个消费者退订后即被删除)
- Arguments(一些消息代理用他来完成类似与 TTL 的某些额外功能)
队列创建
- 队列在声明(declare)后才能被使用。
- 如果一个队列尚不存在,声明一个队列会创建它。
- 如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。
- 如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为 406 的通道级异常就会被抛出。
队列持久化
持久化队列(Durable queues)会被存储在磁盘上,当消息代理(broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。并不是所有的场景和案例都需要将队列持久化。- 持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,只有经过持久化的消息才能被重新恢复。
注意:RabbitMQ中的消息都只能存储在队列中,这一点与Kafka这种消息中间件相反。Kafka将消息存储在topic(主题)这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件的位移标识,RabbitMQ的生产者生产消费并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息井处理,如图2-4 所示

RabbitMQ不支持队列层面的广播消费,如果需要广播消费,则需要在其上进行二次开发。
交换器、路由键、绑定
Exchange: 交换器。在图2-4中我们暂时可以理解成生产者将消息投递到队列中,实际上这个在RabbitMQ中不会发生。真实情况是,生产者将消息发送到Exchange(交换器,通常也可以用大写的“X”来表示),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。这里可以将RabbitMQ 中的交换器看作一个简单的实体,更多的细节会在后面的内容中有所涉及。交换器的具体示意图如图2-5所示。

RabbitMQ 中的交换器有四种类型,不同的类型有着不同的路由策略。
RoutingKey:路由键 。生产者将消息发给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个 RoutingKey需要与交换器类型和绑定键 (BindingKey) 联合使用才能最终生效。
在交换器类型和绑定键 (BindingKey) 固定的情况下,生产者可以在发送消息给交换器时, 通过指定RoutingKey来决定消息流向哪里。
Binding:绑定 。RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键 (BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了,如图2-6所示。

生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型 ,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中 。
注意:
- **在使用绑定的时候,其中需要的路由键是BindingKey。**涉及的客户端方法如channel.exchangeBind、channel.queueBind,对应的AMQP命名为Exchange.Bind、Queue.Bind。
- 在发送消息的时候,其中需要的路由键是RoutingKey。 涉及的客户端方法如channel.basicPublish,对应的AMQP命名为Basic.Publish。
- 后面会有文章介绍如何使用(基于springboot2.0)
交换器类型
RabbitMQ常用的交换器类型有fanout、direct、topic、headers这四种。AMQP协议里还提到另外两种类型: System 和自定义,这里不予描述。对于这四种类型下面一一阐述。
fanout:它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。direct:direct类型的交换器路由规则也很简单,它会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中。
以下图为例,交换器的类型为direct。如果我们发送一条消息,并在发送消息的时候设置 路由键为" warning",则消息 会路由到 Queuel 和 Queue2。如果在发送消息的时候设置路由键为" info" 或者 “debug”,消息只会路由到 Queue2。 如果以其他的路由键发送消息,则消息不会路由到这两个队列中。

topic:前面讲到 direct类型的交换器路由规则是完全匹配 BindingKey和RoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展,它与direct类型的交换器相似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但这里的匹配规则有些不同,它约定:
①RoutingKey为一个点号".“分隔的字符串(被点号”.“分隔开的每一段独立的字符串称为一个单词),如"com.rabbitmq.client”、“com.hidden.client”、
②BindingKey和RoutingKey一样也是点号". “分隔的字符串;
③BindingKey中可以存在两种特殊字符串”*“和”#",用于做模糊匹配,其中"*“用于匹配一个单词,”#“用于匹配多规格单词(可以是零个)。
以图2-8中的配置为例:
①路由键为” com.rabbitmq.client" 的消息会同时路由到 Queuel 和Queue2; · 路由键为" com.hidden.client" 的消息只会路由到Queue2 中;
②路由键为" com.hidden.demo" 的消息只会路由到 Queue2 中;
③路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queuel 中;
④路由键为" java.util.concurrent" 的消息将会被丢弃或者返回给生产者(需要设置mandatory 参数) ,因为它没有匹配任何路由键。

headers:headers类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的headers (也是一个键值对的形式) ,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
RabbitMQ的运转流程
-
生产者发送消息的流程:
①生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
②生产者声明一个交换器,并设置相关属性,比如交换器类型、是否持久化
③生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等
④生产者通过路由键将交换器和队列绑定起来
⑤生产者发送消息至RabbitMQ Broker,其中包括路由键、交换器等信息
⑥相应的交换器根据接收到的路由键查找相匹配的队列。
⑦如果找到,则将从生产者发送过来的消息存入相应的队列中
⑧如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
⑨关闭信道
⑩关闭连接 -
消费者接收消息的流程:
①消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
②消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
③等待RabbitMQ Broker回应并投递相应队列中的消息
④消费者确认(ack)接收到的消息
⑤RabbitMQ从队列中删除相应已经被确认的消息
⑥关闭信道
⑦关闭连接
连接(Connection)和信道(Channel)
我们知道无论是生产者还是消费者,都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。**信道是建立在Connection之上的虚拟连接,RabbitMQ 处理的每条 AMQP 指令都是通过信道完成的。**如图2-9所示

我们完全可以使用 Connection 就能完成信道的工作,为什么还要引入信道呢?试想这样一个场景,一个应用程序中有很多个线程需要从 RabbitMQ中消费消息,或者生产消息,那么必然需要建立很多个Connection,也就是多个TCP连接。然而对于操作系统而言,建立和销毁 TCP连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ采用类似NIO(Non-blocking I/O)的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
注意:NIO,也称非阻塞I/O,包含三大核心部分:Channel(信道)、Buffer(缓冲区)和 Selector(选择器)。NIO 基于Channel和 Buffer 进行操作,数据总是从信道读取数据到缓冲区中,或者从缓冲区写入到信道中。Selector用于监听多个信道的事件(比如连接打开,数据到达等)。因此,单线程可以监听多个数据的信道。另外,NIO中还有一个有名的Reactor模式。
每个线程把持一个信道,所以信道复用了 Connection 的 TCP 连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TCP连接资源。但是信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中,至于这些相关的调优策略需要根据业务自身的实际情况进行调节。
信道在AMQP中是一个很重要的概念,大多数操作都是在信道这个层面展开的。比如 channel.exchangeDeclare、channel.queueDeclare、channel.basicPublish、channel.basicConsume 等方法。RabbitMQ 相关的API与AMQP紧密相连,比如channel.basicPublish对应AMQP的 Basic.Publish 命令。
RabbitMQ命令行与控制台
- 关闭应用 : rabbitmqctl stop_app
- 启动应用 : rabbitmqctl start_app
- 节点状态 : rabbitmqctl status
用户相关 :
- 添加用户 : rabbitmqctl add_user username password
- 列出所有用户 : rabbitmqctl list_users
- 删除用户 : rabbitmqctl delete_user username
- 清除用户权限 : rabbitmqctl clear_permissions -p vhostpath username
- 列出用户权限 : rabbitmqctl list_user_permissions username
- 修改密码 : rabbitmqctl change_password username newpassword
- 设置用户权限 : rabbitmqctl set_permissions -p vhostpath username
虚拟主机相关 :
- 创建虚拟主机 : rabbitmqctl add_vhost vhostpath
- 列出所有虚拟主机 : rabbitmqctl list_vhosts
- 列出虚拟主机上所有权限 : rabbitmqctl list_permissions -p vhostpath
- 删除虚拟主机 : rabbitmqctl delete_vhostpath
队列相关 :
- 查看所有队列信息 : rabbitmqctl list_queues
- 清除队列里的消息 : rabbitmqctl -p vhostpath purge_queue blue
高级操作(主要是集群相关) :
- 移除所有数据 : rabbitmqctl reset, 要在rabbitmqctl stop_app之后使用过
- 组成集群命令 : rabbitmqctl join_cluster [–ram](–ram表示指定数据存储模式, --ram表示数据存储到内存中)
- 查看集群状态 : rabbitmqctl cluster_status
- 修改集群节点的存储形式 : rabbitmqctl change_cluster_node_type disc | ram(ram : 内存;disc : 磁盘)
- 忘记节点(移除节点) : rabbitmqctl forget_cluster_node [–offline](offline表示可以在服务没启动时使用)
- 修改节点名称 : rabbitmqctl rename_cluster_node oldnode1 newnode1 oldnode2 new node2
AMQP协议介绍
从前面的内容可以了解到RabbitMQ是遵从AMQP协议的,换句话说,RabbitMQ就是AMQP 协议的Erlang的实现(当然RabbitMQ还支持STOMP2、MQTTJ等协议)。AMQP的模型架构和RabbitMQ的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的RoutingKey与绑定时的BindingKey相匹配时,消息即被存入相应的队列之中。消费者可以订阅相应的队列来获取消息。
RabbitMQ中的交换器、交换器类型、队列、绑定、路由键等都是遵循的AMQP协议中相应的概念。AMQP协议本身包括三层:
Module Layer: 位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如,客户端可以使用Queue.Declare命令声明一个队列或者使用Basic.Consume订阅消费一个队列中的消息。Session Layer: 位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。Transport Layer: 位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。
AMQP说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level举例来说,AMQP本身是应用层的协议,其填充于TCP协议层的数据部分。而从high-level来说,AMQP 是通过协议命令进行交互的。AMQP协议可以看作一系列结构化命令的集合,这里的命令代表一种操作,类似于HTTP中的方法(GET、POST、PUT、DELETE等)。
AMQP生产者流转过程
为了形象地说明AMQP协议命令的流转过程,这里截取代码清单中的关键代码,代码清单如下所示:

当客户端与Broker建立连接的时候,会调用factory.newConnection方法,这个方法会进一步封装成Protocol Header 0-9-1的报文头发送给Broker, 以此通知Broker本次交互采用的是AMQP0-9-1协议,紧接着Broker返回Connection.Start来建立连接,在连接的过程中涉及Connection.Start/.Start-OK、Connection.Tune/.Tune-Ok、Connection.Open/.Open-Ok这6个命令的交互。
当客户端调用connection.createChannel方法准备开启信道的时候,其包装Channel.Open命令发送给Broker, 等待Channel.Open-Ok命令。
当客户端发送消息的时候,需要调用channel.basicPublish方法,对应的AQMP命令为Basic.Publish, 注意这个命令和前面涉及的命令略有不同,这个命令还包含了Content Header和Content Body。Content Header里面包含的是消息体的属性,例如,投递模式、优先级等,而Content Body包含消息体本身。
当客户端发送完消息需要关闭资源时,涉及Channel.Close/.Close-Ok与 Connection.Close/.Close-Ok的命令交互。详细流转过程如图所示。

AMQP消费者流转过程
本节继续来看消费者的流转过程,参考代码清单1-2, 截取消费端的关键代码如下所示。

流转过程如下:

消费者客户端同样需要与Broker建立连接,与生产者客户端一样,协议交互同样涉及Connection.Start/.Start-Ok、 Connection.Tune/.Tune-Ok 和 Connection. Open/.Open-Ok等。
紧接着也少不了在Connection之上建立Channel, 和生产者客户端一样,协议涉及Channel.Open/Open-Ok。
如果在消费之前调用了channel.basicQos(int prefetchCount)的方法来设置消费者客户端最大能"保持"的未确认的消息数,那么协议流转会涉及Basic.Qos/.Qos-Ok这 两个AMQP命令。
在真正消费之前,消费者客户端需要向Broker发送Basic.Consume命令(即调用channel.basicConsurne方法)将Channel置为接收模式,之后Broker回执Basic.Consume-Ok以告诉消费者客户端准备好消费消息。紧接着Broker向消费者客户端推送(Push)消息,即Basic.Deliver命令,有意思的是这个和Basic.Publish命令一样会携带Content Header和Content Body。
消费者接收到消息并正确消费之后,向Broker发送确认,即Basic.Ack命令。
在消费者停止消费的时候,主动关闭连接,这点和生产者一样,涉及Channel.Close/.Close-Ok禾0 Connection. Close/. Close-Ok。
至此,我们已经了解了RabbitMQ的整体流程。接下来,我们将使用SpringBoot2.X集成RabbitMQ。
本文深入介绍了RabbitMQ的基础理论与核心概念,包括生产者、消费者、队列、交换器、路由键等基本元素的功能及作用。此外,还探讨了RabbitMQ的运转流程、连接与信道的作用机制,以及AMQP协议的基本原理。
1万+

被折叠的 条评论
为什么被折叠?



