RabbitMq
1.初识Mq
1.同步通讯
同步通讯是指发送方在发送消息后,会等待接收方的回应,直到收到回应后才会继续执行后续操作
同步通讯的特点是:
- 阻塞:发送方在等待回应期间会被阻塞,无法进行其他操作
- 顺序执行:消息的处理是按照发送和接收的顺序进行的,确保了消息的时序性
- 实时反馈:发送方可以立即得到接收方的回应,适用于需要立即确认的场景
- 占用资源:由于需要等待,可能会造成资源的浪费,如线程阻塞
打电话就是一个典型的同步通讯例子,通话双方必须实时交流,一方说话时,另一方必须等待
2.异步通讯
异步通讯是指发送方在发送消息后,不需要等待接收方的立即回应,就可以继续执行其他操作。接收方在处理完消息后,可能会在未来的某个时间点给出回应
异步通讯的特点是:
- 非阻塞:发送方在发送消息后可以立即继续其他工作,不会因为等待回应而被阻塞
- 解耦:发送方和接收方在时间上解耦,可以独立处理各自的任务
- 灵活:异步通讯可以处理更复杂的通信模式,如消息队列、事件驱动等
- 资源利用率高:更高效地利用资源,因为不需要等待,可以提高系统的吞吐量
电子邮件是一个异步通讯的例子,你可以发送一封邮件后继续做其他事情,收件人可以在任何时间回复邮件(微信聊天也是一个异步通讯的例子)
异步调用的缺点:
- **不能得到调用结果:**异步调用一般是通知对方执行某个操作,无法知道对方执行操作后的结果
- **不确定下游业务执行是否成功:**异步调用通知下游业务后,无法知道下游业务是否执行成功
- **业务安全依赖于消息代理的可靠性:**下游业务的执行依赖于消息代理的可靠性,一旦消息代理出现故障,下游业务将无法执行
2.MQ 的技术选型

*Kafka 的吞吐量非常高,适合大规模日志场景
3.安装 RabbitMQ 并启动 RabbitMQ
基于dockers安装
//搜索 RabbitMQ 镜像
sudo docker search rabbitmq
//下载 RabbitMQ 镜像
sudo docker pull rabbitmq
// 启动 RabbitMQ
sudo docker run \
-e RABBITMQ_DEFAULT_USER=wuyanzu \
-e RABBITMQ_DEFAULT_PASS=bhoLdSvpd0UAOysh \
-v rabbitmq-plugins:/plugins \
--name rabbitmq \
--hostname rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:latest
指令说明:
-
sudo docker run
: 基本的Docker命令,用于启动一个新的容器实例 -
-e RABBITMQ_DEFAULT_USER=wuyanzu
: 设置RabbitMQ服务的默认用户名为wuyanzu
-
-e RABBITMQ_DEFAULT_PASS=kZoeSW$$xS5i^Cum
: 设置RabbitMQ服务的默认密码为bhoLdSvpd0UAOysh
-
-v rabbitmq-plugins:/plugins
: 将一个名为rabbitmq-plugins
的卷映射到容器的/plugins
目录,用于存放RabbitMQ的插件。这里的rabbitmq-plugins
是一个卷的名称,而不是宿主机的路径 -
--name rabbitmq
: 指定容器的名称为rabbitmq
-
--hostname rabbitmq
: 设置容器的主机名为rabbitmq
-
-p 15672:15672
: 将宿主机的端口15672
映射到容器的端口15672
,这是RabbitMQ管理界面的默认端口 -
-p 5672:5672
: 将宿主机的端口5672
映射到容器的端口5672
,这是RabbitMQ用于AMQP协议通信的默认端口 -
-d
: 在后台运行容器(守护进程) -
rabbitmq:latest
: 使用最新的RabbitMQ官方镜像来创建容器
**访问 RabbitMQ 的管理页面地址:**http://127.0.0.1:15672/
**RabbitMQ 没有安装 Web 插件:**如果开放防火墙端口后还是无法访问 RabbitMQ 的管理界面,可能是安装 RabbitMQ 没有安装 Web 插件
//进入容器内部
sudo docker exec -it rabbitmq bash
//安装 Web 插件
rabbitmq-plugins enable rabbitmq_management
//安装插件后退出容器内部
exit
4.RabbitMQ 入门
**整体架构和核心概念:**RabbitMQ 有几个核心概念:
- Publisher:消息发送者
- Consumer:消息的消费者
- Queue:消息队列,存储消息
- Exchange:交换机,负责路由消息
- VirtualHost:虚拟主机,用于数据隔离


简单样例:
-
**新建队列:**创建一个名为 hello.queue 的队列
-
**绑定队列与交换机:**我们将刚才新创建的 hello.queue 队列与 amq.fanout 交换机绑定(fanout意为扇出)
- 绑定成功界面:
-
**发送消息:**我们在 amq.fanout 交换机中发送一条消息,消息的内容为 Hello, RabbitMQ!
- 查看交换机的总览信息:
- 查看队列中的消息数:
- 查看消息的具体内容:
-
可能遇到的问题:
-
交换机的 overview 页面没有折线图
-
Queues 页面也没有与消息相关的信息
-
点击
channels
后出现Stats in management UI are disabled on this node
信息
需要先修改 RabbitMQ的 配置:
//进入容器内部
sudo docker exec -it rabbitmq bash
//修改配置cd /etc/rabbitmq/conf.d/
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
//先退出容器内部
exit
//再重启容器sudo docker restart rabbitmq
-
5.数据隔离
新建用户:
- 建一个名为 CaiXuKun 的用户,密码为 T1rhFXMGXIOYCoyi ,角色指定为 admin

- 新用户对任意一个 VirtualHost 都是没有访问权限的

- 新用户的账号登录管理台,虽然能看到所有 VirtualHost 的信息,但是无法对任意一个 VirtualHost 进行操作

**为新用户创建一个 VirtualHost:**用新用户的账号登录管理台,创建一个名为 /blog 的 VirtualHost
不同的 VirtualHost 之间有不同的交换机:

6.SpringBoot集成Mq

-
新建一个 SpringBoot 项目,并创建 consumer 和 publisher 两个子模块,项目的整体结构如下:
-
引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
编写与 RabbitMQ 有关的配置信息:
spring: rabbitmq: host: 127.0.0.1 port: 5672 virtual-host: /blog username: CaiXuKun password: T1rhFXMGXIOYCoyi
1.简单的案例

-
创建队列:
-
发送消息:
import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = PublisherApplication.class) public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test void testSendMessageToQueue() { String queueName = "simple.queue"; String msg = "Hello, SpringAMQP!"; rabbitTemplate.convertAndSend(queueName, msg); } }
-
在 RabbitMQ 的控制台可以看到,消息成功发送


-
**接收消息:**SpringAMQP 提供了声明式的消息监听,我们只需要通过
@RabbitListener
注解在方法上声明要监听的队列名称,将来 SpringAMQP 就会把消息传递给使用了@RabbitListener
注解的方法import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class RabbitMQListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String message) { System.out.println("消费者收到了 simple.queue 的消息:【" + message + "】"); } }
7.Work Queues 模型
1.概念
Work Queues,简单地来说,就是让多个消费者绑定到一个队列,共同消费队列中的消息
虽然有多个消费者绑定同一个队列,但是队列中的某一条消息只会被一个消费者消费

简单案例

-
publisher服务的 SpringAmqp 测试类中添加以下方法,该方法可以在 1 秒内产生 50 条消息
@Test void testWorkQueue() throws InterruptedException { String queueName = "work.queue"; for (int i = 1; i <= 50; i++) { String message = "Hello, work queues_" + i; rabbitTemplate.convertAndSend(queueName, message); Thread.sleep(20); } }
-
consumer 服务的 RabbitMQListener 类中添加以下方法,监听 work.queue 队列 ,如果有两个或两个以上的消费者监听同一个队列,默认情况下 RabbitMQ 会采用轮询的方法将消息分配给每个队列,但每个消费者的消费能力可能是不一样的,我们给两个消费者中的代码设置不同的休眠时间,模拟消费能力的不同
@RabbitListener(queues = "work.queue")
public void listenWorkQueue1(String message) throws InterruptedException {
System.out.println("消费者1 收到了 work.queue的消息:【" + message + "】");
Thread.sleep(20);
}@RabbitListener(queues = "work.queue")
public void listenWorkQueue2(String message) throws InterruptedException {
System.err.println("消费者2 收到了 work.queue的消息...... :【" + message + "】");
Thread.sleep(100);
} -
经过测试可以发现,即使两个队列的消费能力不一样 ,默认情况下 RabbitMQ 还是会采用轮询的方法将消息分配给每个队列 ,也就是平均分配,但这不是我们想要的效果,我们想要的效果是消费能力强的消费者处理更多的消息,甚至能够帮助消费能力弱的消费者,怎么样才能达到这样的效果呢,只需要在配置文件中添加以下信息
spring: rabbitmq: listener: simple: prefetch: 1
这个配置信息相当于告诉消费者要一条一条地从队列中取出消息,只有处理完一条消息才能取出下一条
这样一来,就可以充分利用每一台机器的性能,让消费能力强的消费者处理更多的消息,同时还可以避免消息在消费能力较弱的消费者上发生堆积的情况
8.交换机
作用:
- 接收 publisher 发送的消息
- 将消息按照规则路由到与交换机绑定的队列
类型:
-
Fanout:广播
-
概念:Fanout 交换机会将接收到的消息广播到每一个跟其绑定的 queue ,所以也叫广播模式
-
-
Direct:定向
-
概念:Direct 交换机会将接收到的消息根据规则路由到指定的队列,被称为定向路由
- 每一个 Queue 都与 Exchange 设置一个 bindingKey
- 发布者发送消息时,指定消息的 RoutingKey
- Exchange 将消息路由到 bindingKey 与消息 routingKey 一致的队列
-
需要注意的是:同一个队列可以绑定多个 bindingKey ,如果有多个队列绑定了同一个 bindingKey ,就可以实现类似于 Fanout 交换机的效果。由此可以看出,Direct 交换机的功能比 Fanout 交换机更强大
-
-
Topic:话题
-
**概念:**Topic Exchange 与 Direct Exchange类似,区别在于 Topic Exchange 的 routingKey 可以是多个单词的列表(多个 routingKey 之间以
.
分割)
-
- Topic 交换机能实现的功能 Direct 交换机也能实现,不过用 Topic 交换机实现起来更加方便
- 如果某条消息的 topic 符合多个 queue 的 bindingKey ,该条消息会发送给符合条件的所有 queue ,实现类似于 Fanout 交换机的效果
注意事项:交换机只能路由和转发消息,不能存储消息
9.编程式声明队列和交换机
SpringAQMP提供的创建队列和交换机的类
-
SpringAMQP 提供了几个类,用来声明队列、交换机及其绑定关系:
-
Queue:用于声明队列,可以用工厂类 QueueBuilder 构建
-
Exchange:用于声明交换机,可以用工厂类 ExchangeBuilder 构建
-
Binding:用于声明队列和交换机的绑定关系,可以用工厂类 BindingBuilder 构建
-
我们创建一个 Fanout 类型的交换机,并且创建队列与这个交换机绑定,在 consumer 服务中编写 FanoutConfiguration 配置类
import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FanoutConfiguration { @Bean public FanoutExchange fanoutExchange3() { return ExchangeBuilder.fanoutExchange("blog.fanout3").build(); } @Bean public FanoutExchange fanoutExchange4() { return new FanoutExchange("blog.fanout4"); } @Bean public Queue fanoutQueue3() { return new Queue("fanout.queue3"); } @Bean public Queue fanoutQueue4() { return QueueBuilder.durable("fanout.queue4").build(); } @Bean public Binding fanoutBinding3(Queue fanoutQueue3, FanoutExchange fanoutExchange3) { return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange3); } @Bean public Binding fanoutBinding4() { return BindingBuilder.bind(fanoutQueue4()).to(fanoutExchange4()); } }
启动 consumer 的启动类之后,队列、交换机、队列和交换机之间的关系就会自动创建
创建 Queue 时,如果没有指定 durable 属性(持久化行为),则 durable 属性默认为 true
-
编程式声明的缺点
-
当队列和交换机之间绑定的 routingKey 有很多个时,编码将会变得十分麻烦
-
以下是一个队列与 Direct 类型交换机绑定两个 routingKey 时的代码
import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DirectConfiguration { @Bean public DirectExchange directExchange3() { return new DirectExchange("blog.direct3"); } @Bean public Queue directQueue3() { return new Queue("direct.queue3"); } @Bean public Queue directQueue4() { return new Queue("direct.queue4"); } @Bean public Binding directQueue3BindingRed(Queue directQueue3, DirectExchange directExchange3) { return BindingBuilder.bind(directQueue3).to(directExchange3).with("red"); } @Bean public Binding directQueue3BindingBlue(Queue directQueue3, DirectExchange directExchange3) { return BindingBuilder.bind(directQueue3).to(directExchange3).with("blue"); } @Bean public Binding directQueue4BindingRed(Queue directQueue4, DirectExchange directExchange3) { return BindingBuilder.bind(directQueue4).to(directExchange3).with("red"); } @Bean public Binding directQueue4BindingBlue(Queue directQueue4, DirectExchange directExchange3) { return BindingBuilder.bind(directQueue4).to(directExchange3).with("yellow"); } }
-
**注解式声明(推荐使用)--**SpringAMOP 提供了基于
@RabbitListener
注解声明队列和交换机的方式-
我们先在 RabbitMQ 的控制台删除 blog.direct 交换机、 direct.queue1 队列和 direct.queue2 队列
-
改造 consumer 服务的 RabbitMQListener 类的监听 direct.queue1 队列和 direct.queue2 队列的方法
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "blog.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenDirectQueue1(String message) { System.out.println("消费者1 收到了 direct.queue1的消息:【" + message + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "blog.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String message) { System.out.println("消费者2 收到了 direct.queue2的消息:【" + message + "】"); }
-
10.消息转换器

-
在 publisher 服务的 SpringAmqpTests 测试类中新增 testSendObject 方法
@Test void testSendObject() { Map<String, Object> hashMap = new HashMap<>(2); hashMap.put("name", "Tom"); hashMap.put("age", 21); rabbitTemplate.convertAndSend("object.queue", hashMap); }
-
成功发送消息后,我们在 RabbitMQ 的控制台查看消息的具体内容
可以发现,消息的内容类型为 application/x-java-serialized-object,并且消息的内容也变成一堆乱码--我们本来是想发送一个简单的仅含有姓名和年龄两个字段的简短信息,但是消息却变成了一堆乱码,不仅可读性大大下降,而且占用的空间也大大地增加了,这显然不是我们想要的效果
11.消息转换器
默认的消息转换器: Spring 处理对象类型的消息是由 org.springframework.amap.support.converter.MessageConverter
接口来处理的,该接口默认实现是 SimpleMessageConverter
SimpleMessageConverter
类是基于 JDK 提供的 ObjectOutputStream
来类完成序列化的,这种序列化方式存在以下问题:
- 使用 JDK 序列化有安全风险(如果序列化后的消息被恶意篡改,在反序列化的过程中可能会执行一些高危的代码)
- 经过 JDK 序列化的消息占用空间很大
- 经过 JDK 序列化的消息可读性很差
1.自定义消息转换器
采用 JSON 序列化代替默认的 JDK 序列化------引入 jackson 依赖(在项目的父工程中引入)
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
接着在 consumer 服务和 publisher 服务中配置 MessageConverter
@Bean
public MessageConverter jacksonMessageConvertor(){
return new Jackson2JsonMessageConverter();
}
再次发送对象类型的消息,可以看到消息已经成功转换成 JSON 类型的字符串
