一、RabbitMQ简介
1、什么是消息中间件
消息队列中间件是指利用高效可靠的消息传输机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成
消息队列一般有两种传递模式:点对点模式和发布/订阅模式。点对点是基于队列的,消息生产者发送消息到队列,消息消费者从队列中取出消息并消费,队列的存在使得消息的异步传输成为可能。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题,主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用
2、消息中间件的作用
解耦、存储、扩展性、削峰、可恢复性、顺序保证、缓冲、异步通信
- 存储:消息中间件可以把数据进行持久化直到它们已经被完全处理,通过这一方式避免了数据丢失风险
- 顺序保证:大部分消息中间件支持一定程度上的顺序性
- 缓冲:消息中间件通过一个缓冲层来帮助任务最高效率地执行,该缓冲层有助于控制和优化数据流经系统的速度
3、使用Docker操作RabbitMQ
安装并运行rabbitmq
[root@localhost ~]# docker run -d -p 15672:15672 -p 5672:5672 --name rabbitmq rabbitmq:3.6.15-management
默认用户名和密码均为guest
二、RabbitMQ入门
1、相关概念介绍
RabbitMQ整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息
1)、生产者(Producer)和消费者(Consumer)
生产者创建信息,然后发布到RabbitMQ中。消息一般可以包含2个部分:消息体和标签。消息体也可以称之为payload,一般是一个带有业务逻辑结构的数据,消息的标签用来表述这条消息,比如一个交换机的名称和一个路由键。生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者
消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体
2)、虚拟主机(Virtual Host)
一个虚拟主机持有一组交换机、队列和绑定。在RabbitMQ中,用户只能在虚拟主机的粒度进行权限控制。 因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机“/”
3)、队列(Queue)
队列是RabbitMQ的内部对象,用于存储信息。RabbitMQ中消息都只能存储在队列中,这一点和Kafka这种消息中间件相反。Kafka将消息存储在topic这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费
多个消费者可以订阅同一个队列,这时队列中的消息会被轮询给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,RabbitMQ不支持队列层面的广播消费
4)、交换机(Exchange)、路由键(RoutingKey)、绑定(Binding)
生产者将消息发送到交换机,由交换机将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃
生产者将消息发给交换机的时候,一般会指定一个路由键,用来指定这个消息的路由规则,而这个路由键需要与交换机类型和绑定建联合使用才能最终生效
RabbitMQ中通过绑定将交换机与队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就知道如何正确地将消息路由到队列了
生产者将消息发送给交换机时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中
5)、交换机类型
RabbitMQ常用的交换机类型有fanout、direct、topic、headers这四种
1)fanout:
把所有发送到该交换机的消息路由到所有与该交换机绑定的队列中
2)direct:
把消息路由到BindingKey和RoutingKey完全匹配的队列中
下图中交换机的类型为direct,如果我们发送一条信息,并在发送消息的时候设置路由键为“warning”,则消息会路由到Queue1和Queue2。如果在发送消息的时候设置路由键为“info”或者“debug”,消息只会路由到Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中
3)topic:
将消息路由到BindingKey和RoutingKey相匹配的队列中,但这里的匹配规则约定:
- RoutingKey为一个
.
分隔的字符串 - BindingKey和RoutingKey一样也是
.
分隔的字符串 - BindingKey中可以存在两种特殊字符
*
和#
,用于模糊匹配。其中*
用于匹配一个单词,#
用于匹配多规格单词
下图中交换机的类型为topic,如果我们发送一条信息:
- 路由键为“com.rabbitmq.client”的消息会同时路由到Queue1和Queue2
- 路由键为“com.hidden.client”的消息只会路由到Queue2中
- 路由键为“com.hidden.demo”的消息只会路由到Queue2中
- 路由键为“java.rabbitmq.demo”的消息只会路由到Queue2中
- 路由键为“java.util.concurrent”的消息将会被丢弃或者返回给生产者
4)headers:
headers类型的交换机不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换机时制定一组键值对,当发送消息到交换机时,RabbitMQ会获取到该消息的headers,对比其中的键值对是否完全匹配队列和交换机绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers类型的交换机性能很差,而且不实用
6)、RabbitMQ流转流程
生产者发送消息的时候
1)生产者连接到RabbitMQ Broker,建立一个连接,开启一个信道
2)生产者声明一个交换机,并设置相关属性,比如交换机类型、是否持久化等
3)生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等
4)生产者通过路由键将交换机和队列绑定起来
5)生产者发送消息至RabbitMQ Broker,其中包含路由键、交换机等信息
6)相应的交换机根据接收到的路由键查找相匹配的队列
7)如果找到,则从生产者发送过来的消息存入相应的队列中
8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
9)关闭信号
10)关闭连接
消费者接收消息的过程:
1)消费者连接到RabbitMQ Broker,建立一个连接,开启一个信道
2)消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
3)等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息
4)消费者确认接收到的消息
5)RabbitMQ从队列中删除相应已经被确认的消息
6)关闭信道
7)关闭连接
无论是生产者还是消费者,都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道,每个信道都会被指派一个唯一的ID。信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的
RabbitMQ采用类似NIO的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理
每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TCP连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中
2、AMQP协议介绍
AMQP协议本身包括三层:
- Module Layer:位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑
- Session Layer:位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理
- Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等
1)、AMQP生产者流转过程
当客户端与Broker建立连接的时候,会调用factory.newConnection方法,这个方法会进一步封装成Protocol Header 0-9-1的报文头发送给Broker,以此通知Broker本次交互采用的是AMQP 0-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方法,对应的AMQP命令为Basic.Publish,这个命令包含了Content Header和Content Body。Content Header里面包含的是消息体的属性,例如,投递模式、优先级等,而Content Body包含消息体本身
当客户端发送完消息需要关闭资源时,涉及Channel.Close/.Close-Ok与Connection.Close/.Close-Ok的命令交互
2)、AMQP消费者流转过程
消费者客户端同样需要与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.basicConsume方法)将 Channel置为接收模式,之后 Broker回执Basic Consume Ok以告诉消费者客户端准备好消费消息。紧接着Broker 向消费者客户端推送(Push)消息,即Basic.Deliver命令,有意思的是这个和Basic.Publish命令一样会携带Content Header和Content Body
消费者接收到消息并正确消费之后,向Broker发送确认,即Basic.Ack命令
在消费者停止消费的时候,主动关闭连接,这点和生产者 样,涉及Channel Close/.Close-Ok和Connection.Close/.Close-Ok
三、RabbitMQ管理
进入docker容器
[root@localhost ~]# docker exec -it rabbitmq bash
1、多租户与权限
每一个RabbitMQ服务器都能创建虚拟的消息服务器,称为虚拟主机(Virtual Host),简称vhost。每一个本质上都是一个独立的小型RabbitMQ服务器,拥有自己独立的队列、交换机及绑定关系等,并且它拥有自己独立的权限。Virtual Host之间是绝对隔离的,无法将vhost1中的交换机与vhost2中的队列进行绑定,这样既保证了安全性,又可以确保可移植性。RabbitMQ默认创建的vhost为“/”
1)、使用rabbitmqctl add_vhost {vhost}命令创建一个新的vhost,大括号里的参数表示vohost的名称
root@983cebfa9073:/# rabbitmqctl add_vhost vhost1
Creating vhost "vhost1"
2)、删除vhost的命令时rabbitmqctl delete_vhost {vhost}
root@983cebfa9073:/# rabbitmqctl delete_vhost vhost1
Deleting vhost "vhost1"
删除一个vhost同时也会删除其下所有的队列、交换机、绑定关系、用户权限、参数和策略等信息
3)、在RabbitMQ中,权限控制是以vhost为单位的。当创建一个用户时,用户通常会被指派给至少一个vhost,并且只能访问被指派的vhost内的队列、交换机和绑定关系等。因此,RabbitMQ中的授权是指在vhost级别对用户而言的权限授予
相关的授权命令为:rabbitmqctl set_permissions [-p vhost] {user} {conf} {write} {read}
- vhost:授予用户访问权限的vhost名称,可以设置为默认值,即vhost为“/”
- user:可以访问指定vhost的用户名
- conf:一个用于匹配在哪些资源上拥有可配置权限的正则表达式
- write:一个用于匹配在哪些资源上拥有可写权限的正则表达式
- read:一个用于匹配在哪些资源上拥有可读权限的正则表达式
授予root用户可访问虚拟主机vhost1,并在所有资源商都具备可配置、可写及可读的权限
root@983cebfa9073:/# rabbitmqctl set_permissions -p vhost1 root ".*" ".*" ".*"
Setting permissions for user "root" in vhost "vhost1"
4)、列举权限信息
用来显示虚拟主机上的权限:rabbitmqctl list_permissions [-p vhost]
root@983cebfa9073:/# rabbitmqctl list_permissions -p vhost1
Listing permissions in vhost "vhost1"
root .* .* .*
用来显示用户的权限:rabbitmqctl list_user_permissions {username}
root@983cebfa9073:/# rabbitmqctl list_user_permissions root
Listing permissions for user "root"
vhost1 .* .* .*
/ .* .* .*
2、用户管理
在RabbitMQ中,用户是访问控制的基本单位,且单个用户可以跨越多个vhost进行授权。针对一至多个vhost,用户可以被赋予不同级别的访问权限,并使用标准的用户名和密码来认证用户
1)、创建用户的命令为:rabbitmqctl add_user {username} {password}
root@983cebfa9073:/# rabbitmqctl add_user admin admin123
Creating user "admin"
2)、通过rabbitmqctl change_password {username} {password}来更改指定用户的密码
root@983cebfa9073:/# rabbitmqctl change_password admin 123456
Changing password for user "admin"
3)、清除密码:rabbitmqctl clear_password {username}
root@983cebfa9073:/# rabbitmqctl clear_password admin
Clearing password for user "admin"
4)、使用rabbitmqctl authenticate_user {username} {password}通过密码验证用户
root@983cebfa9073:/# rabbitmqctl authenticate_user admin 123456
Authenticating user "admin"
Success
root@983cebfa9073:/# rabbitmqctl authenticate_user admin 123
Authenticating user "admin"
Error: failed to authenticate user "admin"
5)、删除用户的命令是rabbitmqctl delete_use {username}
root@983cebfa9073:/# rabbitmqctl delete_user admin
Deleting user "admin"
6)、rabbitmqctl list_users命令可以用来罗列当前的所有用户。每个结果行都包含用户名称,其后紧跟用户的角色
root@983cebfa9073:/# rabbitmqctl list_users
Listing users
root [administrator]
用户的角色分为5种类型
- none:无任何角色。新创建的用户默认为none
- management:可以访问Web管理界面
- policymaker:包含management的所有权限,并且可以管理策略和参数
- monitoring:包含management的所有权限,并且可以看到所有连接、信道及节点相关信息
- administrator:包含monitoring的所有权限,并且可以管理用户、虚拟主机、权限、策略、参数等。代表了最高权限
7)、用户的角色可以通过rabbitmqctl set_user_tags {username} {tag …}命令设置
root@983cebfa9073:/# rabbitmqctl set_user_tags admin administrator
Setting tags for user "admin" to [administrator]
3、Web端管理
RabbitMQ management插件可以提供Web管理界面用来管理虚拟主机、用户等,也可以用来管理队列、交换机、绑定关系、策略、参数等,还可以用来监控RabbitMQ服务的状态及一些数据统计类信息
访问http://IP:15672/的Web管理界面
四、SpringBoot整合RabbitMQ
1、简单使用
1)、添加依赖
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)、配置文件
spring.rabbitmq.host=192.168.126.151
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
3)、队列配置
@Configuration
public class RabbitConfig {
public static final String QUEUE_A = "queueA";
@Bean
public Queue queue() {
return new Queue(QUEUE_A);
}
}
4)、发送者
@Component
public class MsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String context) {
//不指定exchange时,使用的是(AMQP default)交换机,需要指定一个routingKey和发送到的队列名对应即可
rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_A, context);
}
}
5)、接收者
@Component
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MsgReceiver {
@RabbitHandler
public void process(String context) {
System.out.println("接收到消息:" + context);
}
}
6)、测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class SendMessage {
@Autowired
private MsgSender msgSender;
@Test
public void send() {
for (int i = 0; i < 10; ++i) {
msgSender.send("第" + i + "条消息");
}
}
}
2、一个发送者多个接收者
在上面案例的基础上再添加一个接收者,并标注Receiver1和Receiver2
运行结果:
Receiver2接收到消息:第1条消息
Receiver1接收到消息:第0条消息
Receiver2接收到消息:第3条消息
Receiver2接收到消息:第5条消息
Receiver1接收到消息:第2条消息
Receiver2接收到消息:第7条消息
Receiver1接收到消息:第4条消息
Receiver2接收到消息:第9条消息
Receiver1接收到消息:第6条消息
Receiver1接收到消息:第8条消息
一个发送者多个接收者,会均匀地将消息发送到多个接收者
3、高级使用
1)、发送对象
对象需要实现Serializable接口
发送者
public void sendUser() {
rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_A, new User("jack", 12));
}
接收者
@RabbitHandler
public void process(User user) {
System.out.println(user);
}
运行结果:
User{name='jack', age=12}
2)、Topic Exchange
@Configuration
public class RabbitConfig {
//队列
public static final String QUEUE_A = "topic.message";
public static final String QUEUE_B = "topic.messages";
//交换机
public static final String EXCHANGE = "exchange";
@Bean
public Queue queueA() {
return new Queue(QUEUE_A);
}
@Bean
public Queue queueB() {
return new Queue(QUEUE_B);
}
/**
* Topic Exchange:将消息路由到BindingKey和RoutingKey相匹配的队列中,使用多关键字匹配
* Fanout Exchange:把所有发送到该交换机的消息路由到所有与该交换机绑定的队列中
* Direct Exchange:把消息路由到BindingKey和RoutingKey完全匹配的队列中
* Headers Exchange:通过添加属性key-value匹配
*
* @return
*/
@Bean
TopicExchange topicExchange() {
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding bindingA() {
return BindingBuilder.bind(queueA()).to(topicExchange()).with("topic.message");
}
@Bean
public Binding bindingB() {
return BindingBuilder.bind(queueB()).to(topicExchange()).with("topic.#");
}
}
@Component
public class MsgSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String context) {
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "topic.message", context);
}
public void send2(String context) {
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "topic.messages", context);
}
}
发送send会匹配到topic.message和topic.#监听两个队列的Receiver都可以收到消息,发送send2只会匹配到topic.#只有监听queueB的Receiver可以收到消息
3)、Fanout Exchange
Fanout就是广播模式或者订阅模式,给Fanout交换机发送消息,绑定了这个交换机的所有队列都收到这个消息
@Configuration
public class RabbitConfig {
//队列
public static final String QUEUE_A = "fanoutA";
public static final String QUEUE_B = "fanoutB";
//交换机
public static final String EXCHANGE = "exchange";
@Bean
public Queue queueA() {
return new Queue(QUEUE_A);
}
@Bean
public Queue queueB() {
return new Queue(QUEUE_B);
}
/**
* Topic Exchange:将消息路由到BindingKey和RoutingKey相匹配的队列中,使用多关键字匹配
* Fanout Exchange:把所有发送到该交换机的消息路由到所有与该交换机绑定的队列中
* Direct Exchange:把消息路由到BindingKey和RoutingKey完全匹配的队列中
* Headers Exchange:通过添加属性key-value匹配
*
* @return
*/
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE);
}
@Bean
public Binding bindingA() {
return BindingBuilder.bind(queueA()).to(fanoutExchange());
}
@Bean
public Binding bindingB() {
return BindingBuilder.bind(queueB()).to(fanoutExchange());
}
}
将QUEUE_A和QUEUE_B两个队列绑定到Fanout交换机上,发送端的routing_key会被忽略
@Component
public class MsgSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String context) {
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "", context);
}
}
运行结果:
MsgReceiverA接收QUEUE_A的消息:fanout msg
MsgReceiverB接收QUEUE_B的消息:fanout msg
绑定到fanout交换机上的队列都收到了消息