1. 消息队列概述
- 能够说出什么是消息队列
- 能够安装RabbitMQ
- 能够编写RabbitMQ的入门程序
- 能够说出RabbitMQ的5种模式特征
- 能够使用SpringBoot整合RabbitMQ
目标:能够说出什么是消息队列;为什么使用消息队列;常见产品有哪些
小结:
MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。
MQ的优势和劣势:
优势:
- 应用解耦
- 异步提速
- 削峰填谷
劣势:
- 系统可用性降低
- 系统复杂度提高
- 一致性问题
我们举个例子说明一下消息队列的作用。话说小袁是一家巧克力作坊的老板,生产出美味的巧克力需要三道工序:首先将可可豆磨成可可粉,然后将可可粉加热并加入糖变成巧克力浆,最后将巧克力浆灌入模具,撒上坚果碎,冷却后就是成品巧克力了。
最开始的时候,每次研磨出一桶可可粉后,工人就会把这桶可可粉送到加工巧克力浆的工人手上,然后再回来加工下一桶可可粉。小袁很快就发现,其实工人可以不用自己运送半成品,于是他在每道工序之间都增加了一组传送带,研磨工人只要把研磨好的可可粉放到传送带上,就可以去加工下一桶可可粉了。 传送带解决了上下游工序之间的“通信”问题。
传送带上线后确实提高了生产效率,但也带来了新的问题:每道工序的生产速度并不相同。在巧克力浆车间,一桶可可粉传送过来时,工人可能正在加工上一批可可粉,没有时间接收。不同工序的工人们必须协调好什么时间往传送带上放置半成品,如果出现上下游工序加工速度不一致的情况,上下游工人之间必须互相等待,确保不会出现传送带上的半成品无人接收的情况。
为了解决这个问题,小袁在每组传送的下游带配备了一个暂存半成品的仓库,这样上游工人就不用等待下游工人有空,任何时间都可以把加工完成的半成品丢到传送带上,无法接收的货物被暂存在仓库中,下游工人可以随时来取。传送带配备的仓库实际上起到了“通信”过程中“缓存”的作用。
传送带解决了半成品运输问题,仓库可以暂存一些半成品,解决了上下游生产速度不一致的问题,小袁在不知不觉中实现了一个巧克力工厂版的消息队列。
开发中消息队列通常有如下应用场景:
1、任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
3、削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”
案例:
大多数程序员在面试中,应该都问过或被问过一个经典却没有标准答案的问题:如何设计一个秒杀系统?这个问题可以有一百个版本的合理答案,但大多数答案中都离不开消息队列。
秒杀系统需要解决的核心问题是,如何利用有限的服务器资源,尽可能多地处理短时间内的海量请求。我们知道,处理一个秒杀请求包含了很多步骤,例如:
- 风险控制(监控哪些用户一直是下单不付款的,如果超过一定的阈值,就给这个用户打上标记,归属黑名单)
- 库存锁定
- 生成订单
- 短信通知
- 更新统计数据
如果没有任何优化,正常的处理流程是:App 将请求发送给网关,依次调用上述 5 个流程,然后将结果返回给 APP。
对于对于这 5 个步骤来说,能否决定秒杀成功,实际上只有风险控制和库存锁定这 2 个步骤。只要用户的秒杀请求通过风险控制,并在服务端完成库存锁定,就可以给用户返回秒杀结果了,对于后续的生成订单、短信通知和更新统计数据等步骤,并不一定要在秒杀请求中处理完成。
所以当服务端完成前面 2 个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。
处理一个秒杀请求,从 5 个步骤减少为 2 个步骤,这样不仅响应速度更快,并且在秒杀期间,我们可以把大量的服务器资源用来处理秒杀请求。秒杀结束后再把资源用于处理后面的步骤,充分利用有限的服务器资源处理更多的秒杀请求。
-
实现方式:AMQP,JMS
JMS AMQP 定义 Java api Wire-protocol 跨语言 否 是 跨平台 否 是 模型 提供两种消息模型:Peer-2-Peer,Pub/sub 提供了五种消息模型 支持消息类型 多种消息类型 byte[],当实际应用时,有复杂的消息,可以将消息序列化后发送。 综合评价 JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差; AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语言特性。 -
常见产品:activeMQ,zeroMQ,RabbitMQ,RocketMQ,kafka
三大主流消息队列:
-
rabbitmq:
优点:轻量,迅捷,容易部署和使用,拥有灵活的路由配置
缺点:性能和吞吐量较差(特别是消息堆积情况下性能下降十分严重),每秒钟几万-十几万条,不易进行二次开发(可持续维护性差)
-
rocketmq:
优点:性能好,每秒钟几十万条的处理速度,在线业务的响应延迟在毫秒级,稳定可靠,有活跃的中文社区
缺点:兼容性较差(因为是在中国比较活跃),与周边生态系统的集成要差一些
-
kafka:
优点:强大的性能和吞吐量,每秒钟几十万条,兼容性好(特别是大数据相关的中间件)
缺点:早期的版本消息不可靠(目前版本中已经不存在),设计上需要攒一波再一起发送,所以时延会相对比较高,不大适合在线业务场景
基础介绍:
生产(Producing)只味着发送。发送消息的程序是生产者(Producer):
队列是RabbitMQ中的邮箱的名称。尽管消息流经rabbitmq和应用程序,但它们只能存储在队列中。队列只受主机的内存和磁盘限制的约束,它本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,而许多消费者可以尝试从一个队列接收数据。这就是我们表示队列的方式:
消费和接受有着相似的含义。消费者(Consumer)是一个主要等待接收消息的程序:
注意生产者、消费者和rabbitmq程序不必驻留在同一主机上;事实上,在大多数应用程序中,它们不必驻留在同一主机上。应用程序也可以同时是生产者和消费者。
2. 安装及配置RabbitMQ
目标:按照文档在本机安装windows版本RabbitMQ,并配置其用户和Virtual Hosts
分析:
- 安装erlang;
- 安装rabbitMQ;
- 安装RabbitMQ的图形管理界面插件;
- 创建管理用户;
- 创建虚拟主机Virtual Hosts
小结:
安装RabbitMQ电脑用户中文命名导致启动不了服务解决方案(可以解决)
https://blog.csdn.net/leoma2012/article/details/97636859
安装上述的组件时候都需要使用以管理员身份运行。
虚拟主机安装:
像mysql拥有数据库的概念并且可以指定用户对库和表等操作的权限。RabbitMQ也有类似的权限管理;在RabbitMQ中可以虚拟消息服务器Virtual Host,每个Virtual Hosts相当于一个相对独立的RabbitMQ服务器,每个VirtualHost
之间是相互隔离的。exchange、queue、message不能互通。 相当于mysql的db。Virtual Name一般以/开头。
3. 搭建RabbitMQ入门工程
目标:搭建RabbitMQ入门工程并配置对应的maven依赖
分析:
创建test-rabbitmq的工程;用于测试RabbitMQ的消息收发。添加用于操作RabbitMQ的依赖。
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
小结:
使用IDEA创建maven工程;使用了jdk1.8。在工程中的pom.xml文件中添加了上述的依赖。
4. 入门工程-生产者
目标:编写消息生产者代码,发送消息到队列
分析:
入门工程:生产者发送消息到RabbitMQ的队列(simple_queue);消费者可以从队列中获取消息。可以使用RabbitMQ的简单模式(simple)。
生产者实现发送消息的步骤:
- 创建连接工厂(设置RabbitMQ的连接参数);
- 创建连接;
- 创建频道;
- 声明队列;
- 发送消息;
- 关闭资源
小结:
package com.ittest.rabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 简单模式:发送消息
*/
public class Producer {
static final String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception {
//1. 创建连接工厂(设置RabbitMQ的连接参数);
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机;默认localhost
connectionFactory.setHost("localhost");
//连接端口;默认5672
connectionFactory.setPort(5672);
//虚拟主机;默认/
connectionFactory.setVirtualHost("/ittest");
//用户名;默认guest
connectionFactory.setUsername("test");
//密码;默认guest
connectionFactory.setPassword("test");
//2. 创建连接;
Connection connection = connectionFactory.newConnection();
//3. 创建频道;
Channel channel = connection.createChannel();
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//5. 发送消息;
String message = "你好!小兔纸。";
/**
* 参数1:交换机名称;如果没有则指定空字符串(表示使用默认的交换机)
* 参数2:路由key,简单模式中可以使用队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
//6. 关闭资源
channel.close();
connection.close();
}
}
在设置连接工厂的时候;如果没有指定连接的参数则会有默认值;可以去设置虚拟主机。
虚拟主机和账号密码要与RabbitMq控制台中配置的一致
拓展:
try-with-resources写法优化:
try (Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
...
}
注意,我们可以使用try with resources语句,因为Connection
和Channel
都实现java.io.closeable。这样我们就不需要在代码中显式地关闭它们。
队列参数:
-
Name (队列名)
-
Durable (队列是否持久化,如果是将在broker重新启动后继续存在)
-
Exclusive (仅由一个连接使用,当该连接关闭时,队列将被删除)
-
Auto-delete (至少有一个消费者的队列,在最后一个消费者取消订阅时被删除)
-
Arguments (可选;由插件和特定于代理的功能(如消息ttl、队列长度限制等)使用)
Map<String, Object> args = new HashMap<String, Object>(); args.put("x-max-length", 10); channel.queueDeclare("myqueue", false, false, false, args);
5. 入门工程-消费者
目标:编写消息消费者代码,从队列中接收消息并消费
分析:
从RabbitMQ中队列(与生产者发送消息时的队列一致;simple_queue)接收消息;
实现消费者步骤:
- 创建连接工厂;
- 创建连接;(抽取一个获取连接的工具类)
- 创建频道;
- 声明队列;
- 创建消费者(接收消息并处理消息);
- 监听队列
小结:
package com.ittest.rabbitmq.simple;
import com.ittest.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* 简单模式;消费者接收消息
*/
public class Consumer {
public static void main(String[] args) throws Exception {
//1. 创建连接工厂;
//2. 创建连接;(抽取一个获取连接的工具类)
Connection connection = ConnectionUtil.getConnection();
//3. 创建频道;
Channel channel = connection.createChannel();
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
//5. 创建消费者(接收消息并处理消息);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//接收到的消息
System.out.println("接收到的消息为:" + new String(body, "utf-8"));
}
};
//6. 监听队列
/**
* 参数1:队列名
* 参数2:是否要自动确认;设置为true表示消息接收到自动向MQ回复接收到了,MQ则会将消息从队列中删除;
* 如果设置为false则需要手动确认
* 参数3:消费者
*/
channel.basicConsume(Producer.QUEUE_NAME, true, defaultConsumer);
}
}
需要持续监听队列消息,所以不要关闭资源
DefaultConsumer对象中需要重写handleDelivery方法,用于接收消息
消费者和生产者对于队列的声明参数需要相同,否则会报错
6. 入门工程测试
目标:启动消费者和生产者,到RabbitMQ中查询队列并在消费者端IDEA控制台查看接收到的消息
分析:
生产者:发送消息到RabbitMQ队列(simple_queue)
消费者:接收RabbitMQ队列消息
小结:
简单模式:生产者发送消息到队列中,一个消费者从队列中接收消息。
在RabbitMQ中消费者只能从队列接收消息
工作队列模式:一个消息只能被一个消费者接收,其它消费者是不能接收到同一条消息的。
应用场景:可以在消费者端处理任务比较耗时的时候;添加对同一个队列的消费者来提高任务处理能力。
7. Work queues工作队列模式
目标:编写生产者、消费者代码并测试了解Work queues工作队列模式的特点
分析:
工作队列模式:在同一个队列中可以有多个消费者,消费者之间对于消息的接收是竞争关系。
生产者:发送30个消息
消费者:创建两个消费者监听同一个队列,查看两个消费者的接收消息是否存在重复。
Producer:
package com.ittest.rabbitmq.work;
import com.ittest.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* work工作队列模式:发送消息
*/
public class Producer {
static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
//1. 创建连接工厂(设置RabbitMQ的连接参数);
//2. 创建连接;
Connection connection = ConnectionUtil.getConnection();
//3. 创建频道;
Channel channel = connection.createChannel();
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
for(int i = 1; i <= 30; i++) {
//5. 发送消息;
String message = "你好!小兔纸。work模式 --- " + i;
/**
* 参数1:交换机名称;如果没有则指定空字符串(表示使用默认的交换机)
* 参数2:路由key,简单模式中可以使用队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
}
//6. 关闭资源
channel.close();
connection.close();
}
}
Consumer:
package com.ittest.rabbitmq.work;
import com.ittest.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* work模式;消费者接收消息
*/
public class Consumer1 {
public static void main(String[] args) throws Exception {
//1. 创建连接工厂;
//2. 创建连接;(抽取一个获取连接的工具类)
Connection connection = ConnectionUtil.getConnection();
//3. 创建频道;
final Channel channel = connection.createChannel();
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
//每次可以预取多少个消息
channel.basicQos(1);
//5. 创建消费者(接收消息并处理消息);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//接收到的消息
System.out.println("消费者1---接收到的消息为:" + new String(body, "utf-8"));
Thread.sleep(1000);
//确认消息
/**
* 参数1:消息id
* 参数2:false表示只有当前这条被处理
*/
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//6. 监听队列
/**
* 参数1:队列名
* 参数2:是否要自动确认;设置为true表示消息接收到自动向MQ回复接收到了,MQ则会将消息从队列中删除;
* 如果设置为false则需要手动确认
* 参数3:消费者
*/
channel.basicConsume(Producer.QUEUE_NAME, true, defaultConsumer);
}
}
Consumer2的代码与1基本一致,就不再列出
默认情况下,rabbitmq将按顺序将每个消息发送到下一个使用者。平均每个消费者都会收到相同数量的消息。这种分发消息的方式称为轮询
。
小结:
工作队列模式:一个消息只能被一个消费者接收,其它消费者是不能接收到同一条消息的。
应用场景:可以在消费者端处理任务比较耗时的时候;添加对同一个队列的消费者来提高任务处理能力。
补充:
消息确认
channel.basicConsume(Producer.QUEUE_NAME, false, defaultConsumer);
第二个参数为是否要自动确认;设置为true表示消息接收到自动向MQ回复接收到了,MQ则会将消息从队列中删除; 如果设置为false则需要手动确认。我们可以把这个值设置成false。
然后在循环中通过
//参数1:消息id
//参数2:false表示只有当前这条被删除 ,如果true小于该消息ID的消息都会被删除
channel.basicAck(envelope.getDeliveryTag(), false);
来指定手动的删除已经处理的消息。
在handleDelivery方法执行时,消息状态会由Ready变为UnACK状态,当提交channel.basicAck(envelope.getDeliveryTag(), false);之后,才会真正被消费,否则就可能会出现重复消费了。
QOS:
指的是最多能有多少条消息获取到,但是没有ACK的数量。因为有可能接收到消息之后会提交给其他线程进行处理,处理完之后才进行ACK,如果此时能同时获取多条,就可以提高并行处理效率。
正常测试:两个消费者都订阅同一队列,no_ack均设置为false即开启acknowledge机制,且均未设置prefetch_count,向队列发布5条消息
结果:不管消息是否被ack,rabbitmq会轮流向两个消费者投递消息,第一个消费者收到"1",“3”,“5"三条消息, 第二个消费者收到"2”,"4"两条消息。
prefetch_count设置测试:两个消费者都订阅同一队列,开启acknowledge机制,第一个消费者prefetch_count设置为1,另一个消费者未设置prefetch_count,同样向队列发布5条消息
结果:rabbitmq向第一个消费者投递了一条消息后,消费者未对该消息进行ack,rabbitmq不会再向该消费者投递消息,剩下的四条消息均投递给了第二个消费者
8. 订阅模式类型说明
目标:说出订阅模式中的Exchange交换机作用以及交换机的三种类型
小结:
订阅模式与前面的两种模式比较:多了一个角色Exchange交换机,接收生产者发送的消息并决定如何投递消息到其绑定的队列;消息的投递决定于交换机的类型。
交换机类型:广播(fanout)、定向(direct)、通配符(topic)
交换机只做消息转发,自身不存储数据。
目标:说出订阅模式中的Exchange交换机作用以及交换机的三种类型
小结:
订阅模式与前面的两种模式比较:多了一个角色Exchange交换机,接收生产者发送的消息并决定如何投递消息到其绑定的队列;消息的投递决定于交换机的类型。
广播(fanout)、定向(direct)、通配符(topic)
Exchange:交换机。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给
某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常
见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
交换机只做消息转发,自身不存储数据。
9. Publish/Subscribe发布与订阅模式
目标:编写生产者、消费者代码并测试了解Publish/Subscribe发布与订阅模式的特点
分析:
发布与订阅模式特点:一个消息可以被多个消费者接收;其实是使用了订阅模式,交换机类型为:fanout广播
- 生产者(发送10个消息)
- 创建连接;
- 创建频道;
- 声明交换机(fanout);
- 声明队列;
- 队列绑定到交换机;
- 发送消息;
- 关闭资源
//3. 声明交换机;参数1:交换机名称,参数2:交换机类型(fanout,direct,topic)
channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);
//5. 队列绑定到交换机;参数1:队列名称,参数2:交换机名称,参数3:路由key
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHANGE, "");
channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHANGE, "");
channel.basicPublish(FANOUT_EXCHANGE, "", null, message.getBytes());
交换机类型声明为:BuiltinExchangeType.FANOUT
路由Key不用声明
- 消费者(至少两个消费者)
- 创建连接;
- 创建频道;
- 声明交换机;
- 声明队列;
- 队列绑定到交换机;
- 创建消费者;
- 监听队列;
//3. 声明交换机
channel.exchangeDeclare(Producer.FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null);
//5. 队列绑定到交换机上
channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHANGE, "");
channel.basicConsume(Producer.FANOUT_QUEUE_1, true, defaultConsumer);
小结:
发布与订阅模式:一个消息可以被多个消费者接收;一个消费者对于的队列,该队列只能被一个消费者监听。使用了订阅模式中交换机类型为:广播。
10. Routing路由模式
目标:编写生产者、消费者代码并测试了解Routing路由模式的特点
分析:
生产者:发送两条消息(路由key分别为:insert、update)
//3. 声明交换机;参数1:交换机名称,参数2:交换机类型(fanout,direct,topic)
channel.exchangeDeclare(DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null);
channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null);
//5. 队列绑定到交换机;参数1:队列名称,参数2:交换机名称,参数3:路由key
channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHANGE, "insert");
channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHANGE, "update");
//6. 发送消息;
String message = "你好!小兔纸。路由模式 ;routing key 为 insert ";
/**
* 参数1:交换机名称;如果没有则指定空字符串(表示使用默认的交换机)
* 参数2:路由key,简单模式中可以使用队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHANGE, "insert", null, message.getBytes());
System.out.println("已发送消息:" + message);
message = "你好!小兔纸。路由模式 ;routing key 为 update ";
/**
* 参数1:交换机名称;如果没有则指定空字符串(表示使用默认的交换机)
* 参数2:路由key,简单模式中可以使用队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHANGE, "update", null, message.getBytes());
System.out.println("已发送消息:" + message);
交换机类型声明为:BuiltinExchangeType.DIRECT
路由KEY必须声明,根据路由KEY由交换机判断是否要发送
消费者:创建两个消费者,监听的队列分别绑定路由key为:insert、update
- 消息中路由key为insert的会被绑定路由key为insert的队列接收并被其监听的消费者接收、处理;
- 消息中路由key为update的会被绑定路由key为update的队列接收并被其监听的消费者接收、处理;
//3. 声明交换机
channel.exchangeDeclare(Producer.DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);
//4. 声明队列;
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列(消息会持久化保存在服务器上)
* 参数3:是否独占本连接
* 参数4:是否在不使用的时候队列自动删除
* 参数5:其它参数
*/
channel.queueDeclare(Producer.DIRECT_QUEUE_INSERT, true, false, false, null);
//5. 队列绑定到交换机上
channel.queueBind(Producer.DIRECT_QUEUE_INSERT, Producer.DIRECT_EXCHANGE, "insert");
小结:
Routing 路面模式要求队列绑定到交换机的时候指定路由key;消费发送时候需要携带路由key;只有消息的路由key与队列路由key完全一致才能让该队列接收到消息。
11. Topics通配符模式
目标:编写生产者、消费者代码并测试了解Topics通配符模式的特点
分析:
Topic 类型与Direct 相比,都是可以根据RoutingKey 把消息路由到不同的队列。只不过Topic 类型Exchange 可
以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
item.#(0个或者多个) :能够匹配item.insert.abc 或者 item.insert
item.* (1个):只能匹配item.insert
- 生产者:发送包含有item.insert、item.update,item.delete的3种路由key消息
- 消费者1:监听的队列绑定到交换机的路由key为:item.update,item.delete
- 消费者2:监听的队列绑定到交换机的路由key为:item.*
小结:
Topics通配符模式:可以根据路由key将消息传递到对应路由key的队列;队列绑定到交换机的路由key可以有多个;通配符模式中路由key可以使用 *
和 #
;使用了通配符模式之后对于路由Key的配置更加灵活。
12. RabbitMQ模式总结
目标:对比总结RabbitMQ的5种模式特征
小结:
- 不直接使用Exchange交换机(默认交换机)
- simple简单模式:一个生产者生产一个消息到一个队列被一个消费者接收
- work工作队列模式:生产者发送消息到一个队列中,然后可以被多个消费者监听该队列;一个消息只能被一个消费者接收,消费者之间是竞争关系
- 使用Exchange交换机;订阅模式(交换机:广播fanout、定向direct、通配符topic)
- 发布与订阅模式:使用了fanout广播类型的交换机,可以将一个消息发送到所有绑定了该交换机的队列
- 路由模式:使用了direct定向类型的交换机,消费会携带路由key,交换机根据消息的路由key与队列的路由key进行对比,一致的话那么该队列可以接收到消息
- 通配符模式:使用了topic通配符类型的交换机,消费会携带路由key(*, #),交换机根据消息的路由key与队列的路由key进行对比,匹配的话那么该队列可以接收到消息
13. Spring 整合RabbitMQ
添加依赖
修改pom.xml文件内容为如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ittest</groupId>
<artifactId>spring-rabbitmq-producer</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
</project>
配置整合
- 创建
spring-rabbitmq-producer\src\main\resources\properties\rabbitmq.properties
连接参数等配置文件;
rabbitmq.host=192.168.12.135
rabbitmq.port=5672
rabbitmq.username=ittest
rabbitmq.password=ittest
rabbitmq.virtual-host=/ittest
- 创建
spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
整合配置文件;
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:properties/rabbitmq.properties"/>
<!-- 定义rabbitmq connectionFactory -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<!--定义管理交换机、队列-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机
默认交换机类型为direct,名字为:"",路由键为队列的名称
-->
<rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~广播;所有队列都能收到消息~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>
<!--定义广播类型交换机;并绑定上述两个队列-->
<rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding queue="spring_fanout_queue_1"/>
<rabbit:binding queue="spring_fanout_queue_2"/>
</rabbit:bindings>
</rabbit:fanout-exchange>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~通配符;*匹配一个单词,#匹配多个单词 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>
<rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="ittest.*" queue="spring_topic_queue_star"/>
<rabbit:binding pattern="ittest.#" queue="spring_topic_queue_well"/>
<rabbit:binding pattern="ittest1.#" queue="spring_topic_queue_well2"/>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
发送消息
创建测试文件 spring-rabbitmq-producer\src\test\java\com\ittest\rabbitmq\ProducerTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 只发队列消息
* 默认交换机类型为 direct
* 交换机的名称为空,路由键为队列的名称
*/
@Test
public void queueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。");
}
/**
* 发送广播
* 交换机类型为 fanout
* 绑定到该交换机的所有队列都能够收到消息
*/
@Test
public void fanoutTest(){
/**
* 参数1:交换机名称
* 参数2:路由键名(广播设置为空)
* 参数3:发送的消息内容
*/
rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "发送到spring_fanout_exchange交换机的广播消息");
}
/**
* 通配符
* 交换机类型为 topic
* 匹配路由键的通配符,*表示一个单词,#表示多个单词
* 绑定到该交换机的匹配队列能够收到对应消息
*/
@Test
public void topicTest(){
/**
* 参数1:交换机名称
* 参数2:路由键名
* 参数3:发送的消息内容
*/
rabbitTemplate.convertAndSend("spring_topic_exchange", "ittest.bj", "发送到spring_topic_exchange交换机ittest.bj的消息");
rabbitTemplate.convertAndSend("spring_topic_exchange", "ittest.bj.1", "发送到spring_topic_exchange交换机ittest.bj.1的消息");
rabbitTemplate.convertAndSend("spring_topic_exchange", "ittest.bj.2", "发送到spring_topic_exchange交换机ittest.bj.2的消息");
rabbitTemplate.convertAndSend("spring_topic_exchange", "ittest.cn", "发送到spring_topic_exchange交换机ittest.cn的消息");
}
}
搭建消费者工程
添加依赖
修改pom.xml文件内容为如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ittest</groupId>
<artifactId>spring-rabbitmq-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
</dependencies>
</project>
配置整合
- 创建
spring-rabbitmq-consumer\src\main\resources\properties\rabbitmq.properties
连接参数等配置文件;
rabbitmq.host=192.168.12.135
rabbitmq.port=5672
rabbitmq.username=ittest
rabbitmq.password=ittest
rabbitmq.virtual-host=/ittest
- 创建
spring-rabbitmq-consumer\src\main\resources\spring\spring-rabbitmq.xml
整合配置文件;
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:properties/rabbitmq.properties"/>
<!-- 定义rabbitmq connectionFactory -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<bean id="springQueueListener" class="com.ittest.rabbitmq.listener.SpringQueueListener"/>
<bean id="fanoutListener1" class="com.ittest.rabbitmq.listener.FanoutListener1"/>
<bean id="fanoutListener2" class="com.ittest.rabbitmq.listener.FanoutListener2"/>
<bean id="topicListenerStar" class="com.ittest.rabbitmq.listener.TopicListenerStar"/>
<bean id="topicListenerWell" class="com.ittest.rabbitmq.listener.TopicListenerWell"/>
<bean id="topicListenerWell2" class="com.ittest.rabbitmq.listener.TopicListenerWell2"/>
<rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
<rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
<rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
<rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
<rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>
</rabbit:listener-container>
</beans>
消息监听器
1)队列监听器
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\SpringQueueListener.java
public class SpringQueueListener implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2)广播监听器1
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\FanoutListener1.java
public class FanoutListener1 implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("广播监听器1:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3)广播监听器2
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\FanoutListener2.java
public class FanoutListener2 implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("广播监听器2:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4)星号通配符监听器
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\TopicListenerStar.java
public class TopicListenerStar implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("通配符*监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
5)井号通配符监听器
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\TopicListenerWell.java
public class TopicListenerWell implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("通配符#监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
6)井号通配符监听器2
创建 spring-rabbitmq-consumer\src\main\java\com\ittest\rabbitmq\listener\TopicListenerWell2.java
public class TopicListenerWell2 implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("通配符#监听器2:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n",
message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(),
message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
13. 创建SpringBoot整合 的两个工程
目标:创建springboot-rabbitmq-producer工程用于生产消息;创建springboot-rabbitmq-consumer工程用于接收消息
分析:
Spring Boot提供了对于AMQP的整合;可以使用RabbitTemplate发送消息;可以使用@RabbitListener注解接收消息。
生产者工程springboot-rabbitmq-producer:发送消息
- 创建工程;
- 添加依赖(spring-boot-stater-amqp,spring-boot-starter-test);
- 创建启动引导类;
- 添加配置文件application.yml
消费者工程springboot-rabbitmq-consumer:接收消息
- 创建工程;
- 添加依赖(spring-boot-stater-amqp);
- 创建启动引导类;
- 添加配置文件application.yml
小结:
可以使用插件自动生产Spring Boot工程的启动引导类Application.java和配置文件application.yml
14. 配置生产者工程
目标:配置springboot-rabbitmq-producer工程的RabbitMQ,一个交换机、队列并绑定
分析:
使用通配符模式:将队列绑定到交换机(topic)时需要指定路由key(item.#)
- 配置RabbitMQ的连接参数:主机、连接端口、虚拟主机、用户名、密码;
- 声明交换机、队列并将队列绑定到交换机,指定的路由key(item.#)
小结:
- 配置application.yml文件
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /ittest
username: test
password: test
- 配置交换机、队列和绑定,创建一个配置类
@Configuration
public class RabbitMQConfig {
//交换机名称
public static final String ITEM_TOPIC_EXCHANGE = "item_topic_exchange";
//队列名称
public static final String ITEM_QUEUE = "item_queue";
//声明交换机
@Bean("itemTopicExchange")
public Exchange topicExchange(){
return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
}
//声明队列
@Bean("itemQueue")
public Queue itemQueue(){
return QueueBuilder.durable(ITEM_QUEUE).build();
}
//将队列绑定到交换机
@Bean
public Binding itemQueueExchange(@Qualifier("itemQueue") Queue queue,
@Qualifier("itemTopicExchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
}
}
15. 配置消费者工程
目标:配置springboot-rabbitmq-consumer工程的RabbitMQ,编写消息监听器接收消息
分析:
- 配置application.yml文件,设置RabbitMQ的连接参数;
- 编写消息监听器接收队列(item_queue)消息;可以使用注解@RabbitListener接收队列消息
小结:
- 配置application.yml文件;与生产者工程一致
- 编写监听器类
@Component
public class MyListener {
/**
* 接收队列消息
* @param message 接收到的消息
*/
@RabbitListener(queues = "item_queue")
public void myListener1(String message){
System.out.println("消费者接收到消息:" + message);
}
}
接收消息的队列名称要与生产者发送消息时的队列名称一致
16. 测试消息发送和接收
目标:生产者编写测试类RabbitMQTest发送消息到交换机和特定的路由(item.insert,item.update,item.delete)
分析:
生产者:编写测试类RabbitMQTest,利用RabbitTemplate发送3条消息,这3条消息的路由key分别是item.insert,item.update,item.delete
消费者:在IDEA控制台查看是否能接收到符合路由key的消息
小结:
编写测试类如下:
package com.ittest.rabbitmq;
import com.ittest.rabbitmq.config.RabbitMQConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE,
"item.insert", "商品新增,路由Key为item.insert");
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE,
"item.update", "商品新增,路由Key为item.update");
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE,
"item.delete", "商品新增,路由Key为item.delete");
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE,
"a.item.delete", "商品新增,路由Key为a.item.delete");
}
}
先启动测试类进行声明交换机、队列和绑定;之后再启动消费者工程接收消息
补充:
Fanout模式:https://blog.csdn.net/m0_37034294/article/details/82863334