RabbitMQ
文章目录
前记:
- 资料来源:尚硅谷(感谢)
- 明年的春招加油
- –于2021.10.13
一、消息队列
1基本概念
什么是MQ
MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是
message 而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常
见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不
用依赖其他服务。
MQ – 一种通信机制。
为什么要用MQ
- 流量消峰:相当于一个缓冲,防止系统宕机
- 应用解耦:服务与服务之间的缓冲带
- 异步处理:作回送消息缓冲带
MQ的分类
- ActiveMQ
- Kafka:大数据
- .RocketMQ:阿里出品,经过双11考验
- RabbitMQ:Elang语言带来的高并发特性
MQ的选择
根据四大MQ的特点来选择即可。
2RabbitMQ
基本概念
- RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。
- 用Erlang语言编写
四大核心名词
生产者
产生数据发送消息的程序是生产者
交换机
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息
推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推
送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存
储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可
以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费
者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
核心模式部分
工作原理
**Broker:**接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
**Virtual host:**出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似
于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出
多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等
**Connection:**publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP
Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程
序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客
户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。 l 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 的分发依据
安装
详细看文档
二、Hello World
1依赖
<!-- 指定 jdk 编译版本 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!--rabbitmq 依赖客户端 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!-- 操作文件流的一个依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
2生产者
public class Producer {
public static final String QUEUE_NAME = "myQueue";
public static void main(String[] args) throws Exception{
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("xx.xxx.xxx.xxx");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("123");
//得到连接
Connection connection = connectionFactory.newConnection();
//得到信道 及 队列
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//发送 (用默认的交换机)
String message = "hello world!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("生产OK");
}
}
3消费者
public class Consumer {
public static void main(String[] args) throws Exception{
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("xx.xxx.xxx.xxx");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("123");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = new DeliverCallback() { //接收成功回调接口
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println("consumerTag = " + consumerTag);
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
}
};
CancelCallback cancelCallback = consumerTag -> {//消费失败的回调接口
System.out.println("失败啦!!consumerTag = " + consumerTag);
};
channel.basicConsume(Producer.QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
三、Work Queues
工作模式,又称简单队列
1轮询分发消息
- 指的是:如果有多个消费者共享一条队列,那么队列会依次分发消息给每一个消费者
2消息应答
基本概念
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,
消息应答就是: 消费者在接收到消息并且处理该消息之后,告诉rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了
自动应答
- 机制:消息发送后立即被认为已经被消费成功
- 缺点:
- 很有可能消费者没有完全消费而宕机,导致消息的丢失
- 在大量的消息分发下,消费者来不及消费,导致消息的堆积,导致宕机
- 应用场景:这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用
手动应答
- Channel.basicAck(用于肯定确认):RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
- Channel.basicNack(用于否定确认):即消费没成功,不能丢弃
- Channel.basicReject(用于否定确认):与 Channel.basicNack 相比少一个参数(boolean multiple), 不处理该消息了直接拒绝,可以将其丢弃了
批量应答Multipe
channel.basicAck(deliveryTag, true)
- true:代表批量应答 channel 上未应答的消息。如n个消息到达消费者,消费者只要消费了一条消息,就发送批量应答消息,代表这n个消息全部被消费成功,实际这里只消费了一个消息。
- false:只会应答已经被处理的消息
- 手动应答的好处是可以批量应答并且减少网络拥堵
消息自动重新入队
- 如果没有收到消费者的ACK应答消息,RabbitMQ 将了解到消息未完全处理,并将对其重新排队(排在队头)
- 代码演示:
public class RabbitMQUtils {
public static Channel getChannel(){
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("xx.xxx.xxx.xxx");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("123");
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
channel = connection.createChannel();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return channel;
}
}
public class Producer {
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException {
Channel channel = RabbitMQUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.next();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送了:" + message);
}
}
}
public class Work01 {
public static void main(String[] args) throws IOException {
Channel channel = RabbitMQUtils.getChannel();
System.out.println("01处理消息快!");
DeliverCallback deliverCallback = new DeliverCallback() { //接收成功回调接口
public void handle(String consumerTag, Delivery message) throws IOException {
try {
Thread.sleep(1 * 1000); //模拟处理速度
} catch (InterruptedException e) {
e.printStackTrace();
}
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
/**
* 手动应答代码:
* message.getEnvelope().getDeliveryTag() :消息的唯一标志
* false :不用批量应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
CancelCallback cancelCallback = consumerTag -> {//消费失败的回调接口
System.out.println("失败啦!!consumerTag = " + consumerTag);
};
//关闭了自动应答
channel.basicConsume(Producer.QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
public class Work02 {
public static void main(String[] args) throws IOException {
Channel channel = RabbitMQUtils.getChannel();
System.out.println("02处理消息慢!");
DeliverCallback deliverCallback = new DeliverCallback() { //接收成功回调接口
public void handle(String consumerTag, Delivery message) throws IOException {
try {
Thread.sleep(15 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
/**
* 手动应答代码:
* message.getEnvelope().getDeliveryTag() :消息的唯一标志
* false :不用批量应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
CancelCallback cancelCallback = consumerTag -> {//消费失败的回调接口
System.out.println("失败啦!!consumerTag = " + consumerTag);
};
channel.basicConsume(Producer.QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
模拟的情况如下:
生产者发送消息1和2,可以见到消息以轮询的方式工作。
生产者发送消息3和4,见到3被01线程立即处理。
而消息4,正在被02线程处理。
此时,把02线程关闭。
可以见到,由于没有应答,所以把消息4重新分发给了线程01。
1
发送了:1
2
发送了:2
3
发送了:3
4
发送了:4
01处理消息快!
msg = 1
msg = 3
msg = 4
02处理消息慢!
msg = 2
3持久化
基本概念
-
如果队列和消息不进行持久化,那么队列和消息是存储内存之中的,如果断电或者RMQ重启,那么队列和消息就会消失。
-
要保证消息和队列不丢失,那么要将队列和消息进行持久化到磁盘中去。
队列的持久化
-
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
在声明队列的时候,把第二个参数durable改成true即可。 -
注意:如果已经存在了同名的不持久化队列,那么再声明会报错
在管理界面可以看到相关的信息
消息的持久化
-
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
把第三个参数props, 传入MessageProperties.PERSISTENT_TEXT_PLAIN
参数。 -
注意:持久化不能保证消息一定不丢失,如在保存在磁盘的过程中断电或重启了
不公平分发
- 如果是0:
channel.basicQos(0);
默认参数是0,表示是公平分发,即轮询分发 - 如果是1:则轮询,看谁空闲就给谁, 是一种比较特殊的预取值
- 如果是其他值,那么就成了预取值
- 应用场景:消费的者的消费速度不一致,如果进行轮询分发,会导致有些处理速度慢的消费者消息积压,而处理速度快的消费者无事可做
预取值
- 预取值:实际上是一个未确认的消息缓冲区,通过设置该缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题
- 代码:
channel.basicQos(int preFetch);
- 所以channel.basicQos在消费者端设置,即设置未确认的消息缓冲区的大小
四、发布确认
1发布确认原理
- 机制:当生产者发布消息的时候,MQ如果收到了,会发送一个信号,告诉生产者MQ已经成功接收了
- 持久化情况:当MQ持久化到磁盘,才会通知生产者
- 否定确认:如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
- 批量确认:此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。(注意和生产者的批量确认发布区别开)
2发布确认的策略
开启发布确认
channel.confimSelect()
在生产者上开启。
单个确认发布
- 特点:发布一条信息就等待MQ的确认消息
- 优点:保证消息的正确发布
- 缺点:慢
批量确认发布
- 特点:发布批量消息后,才等待MQ的确认消息
- 优点:与单个确认相比,可以提高吞吐量
- 缺点:如果出现问题,不能知道是具体是哪个消息出现了问题
- 出现问题的解决方法:把发送的批消息存储在内存中,出现问题批量重发
异步确认发布
- 通过一个中间件来存储确认发布成功与否的消息
三种确认发布代码实现
关键代码:
- 开启发布确认:channel.confirmSelect();
- 等待发布确认:channel.waitForConfirms(); //异步的话就不用等待了
- 异步监听:channel.addConfirmListener(ackCallback, nackCallback);
public class Producer {
public static void main(String[] args) throws Exception {
publicSingle();//publicSingle花费了:16791ms
System.out.println("-------------------------------------------");
publicBatch();
System.out.println("--------------------------------------------");
publicSyn();
}
//单个确认模式
public static void publicSingle() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
channel.confirmSelect(); //-----------------开启发布确认
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = "消息" + i;
channel.basicPublish("", queueName, null, message.getBytes());
boolean confirms = channel.waitForConfirms();//-----------------等待发布确认
if (!confirms) {
System.out.println(message + "发布失败");
}
}
long end = System.currentTimeMillis();
System.out.println("publicSingle花费了:" + (end - begin) + "ms");
}
//批量确认模式
public static void publicBatch() throws Exception {
Channel channel = RabbitMQUtils.getChannel();
channel.confirmSelect(); //-----------------开启发布确认
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = "消息" + i;
channel.basicPublish("", queueName, null, message.getBytes());
if ((i + 1) % 100 == 0) { //每隔100条确认
boolean confirms = channel.waitForConfirms();//-----------------等待发布确认
if (!confirms) {
System.out.println(message + "发布失败");
}
}
}
long end = System.currentTimeMillis();
System.out.println("publicBatch花费了:" + (end - begin) + "ms");
}
//异步确认模式
public static void publicSyn() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
channel.confirmSelect(); //-----------------开启发布确认
ConfirmCallback ackCallback = (deliveryTag, multiple) ->{ //注意,确认和未确认回调接口,都一个类型的,即ConfirmCallback
if(!multiple) {
System.out.println("MQ未开启批量确认");
}
System.out.println(deliveryTag + "号消息成功发送了");
};
ConfirmCallback nackCallback = (deliveryTag, multiple) ->{
if(!multiple) {
System.out.println("MQ未开启批量确认");
}
System.out.println(deliveryTag + "号消息成功失败了");
};
channel.addConfirmListener(ackCallback, nackCallback);//其中一个值可以是null,代表不监听成功或失败的确认回调
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = "消息" + i;
channel.basicPublish("", queueName, null, message.getBytes());
if ((i + 1) % 100 == 0) { //每隔100条确认
boolean confirms = channel.waitForConfirms();//-----------------等待发布确认
if (!confirms) {
System.out.println(message + "发布失败");
}
}
}
long end = System.currentTimeMillis();
System.out.println("publicSyn花费了:" + (end - begin) + "ms");
}
}
publicSingle花费了:14840ms
-------------------------------------------
publicBatch花费了:40ms
--------------------------------------------
13号消息成功发送了
19号消息成功发送了
26号消息成功发送了
35号消息成功发送了
45号消息成功发送了
49号消息成功发送了
60号消息成功发送了
71号消息成功发送了
83号消息成功发送了
87号消息成功发送了
99号消息成功发送了
MQ未开启批量确认 //开启了多线程来进行处理。
100号消息成功发送了
.....
publicSyn花费了:310ms
处理异步未确认发布的消息
步骤:
- 加入第一个发送的消息到map中
- 如果在确认接口中回调,则清除。(注意MQ批量确认的代码,即multiple=true的情况)
- 全部确认的消息消除完毕后,剩下的就是未确认的
public static void publishMessageAsync() throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况
* 1. 轻松的将序号与消息进行关联
* 2. 轻松批量删除条目 只要给到序列号
* 3. 支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new
ConcurrentSkipListMap<>();
/**
* 确认收到消息的一个回调
* 1. 消息序列号
* 2.true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
// 返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNumber, true);
// 清除该部分未确认消息
confirmed.clear();
}else{
// 只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
String message = outstandingConfirms.get(sequenceNumber);
System.out.println(" 发布的消息"+message+" 未被确认,序列号"+sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1. 确认收到消息的回调
* 2. 未收到消息的回调
*/
channel.addConfirmListener(ackCallback, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = " 消息" + i;
/**
* channel.getNextPublishSeqNo() 获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println(" 发布" + MESSAGE_COUNT + " 个异步确认消息, 耗时" + (end - begin) +
"ms");
}
}
五、交换机
1基本概念
Exchange概念
- 生产者生产的消息从来不会直接发送到队列
- 生产者只能将消息发送到交换机
- 交换机的工作内容:
- 接收生产者的消息
- 将消息推送到对应的队列
- 注意:交换机的消息可以被推送到多个队列,而队列中的消息一定只能被消费一次。
Exchanges 的类型
总共有以下类型:
直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
无名 exchange
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
其中的空字符串代表的是默认交换机
2临时队列
- 创建一个临时队列:
String queueName = channel.queueDeclare().getQueue();
- 创建一个具有随机名称的队列
- 一旦我们断开了消费者的连接,队列将被自动删除
3 绑定(bindings)
- 是连接交换机和队列的桥梁。交换机和队列是独立的,它们之间的关系是通过绑定连接的。
- 一个队列可以有多个绑定
- 绑定和routingKey的关系:绑定是通过routingKey来实现的
4Fanout Exchange
fanout介绍
- 是订阅广播模式,一旦队列与该类型的交换机绑定,那么只要交换机收到消息,就会转发到所有与其绑定的队列
- 注意:在Fanout交换机下,routingKey是没有作用的。发布者在发布的时候,可以把routingKey置为“”。如
channel.basicPublish("", "", null, message.getBytes());
。即消息的发送到队列,只需要指定交换机名称
5Direct Exchange
介绍
- 消息的发送到队列,需要同时指定交换机名称和绑定的routingKey
多重绑定
- 如果一个交换机,用一个routingKey绑定多个队列,那么此时称为多重绑定
- 在多重绑定,指明交换机名称和routingKey,可以做到类似于fanout交换机的效果
6Topic Exchange
介绍
- 提供了一种模糊的绑定
- *(星号)可以代替一个单词
#(井号)可以替代零个或多个单词 - 如:中间带 orange 带 3 个单词的字符串(*.orange.*)
- 是最强大的交换机类型:
- 如果当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
- 如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
- 其他情况便是模糊绑定
7实战代码
注意点
需要注意的是,本章学习的是交换机,及交换机与队列之间的绑定,与消费者的代码其实是没有关系的。
交换机的类型声明与绑定,在生产者(消费者也行)中声明即可。
生产者要发布消息,需要指明交换机和绑定。
消费者仍然面向队列来消费消息。
关键代码:
- 声明交换机:
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
- 声明绑定:
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
- 发布消息(同时指定交换机和routingKey,如果是fanout交换机,那么routingKey是无效的):
channel.basicPublish(EXCHANGE_NAME,rountingKey, null,message.getBytes("UTF-8"));
六、死信队列
1死信的概念
- 死信(dead letter):指的是无法被消费的消息
- 死信会经过死信交换机,放入到死信队列中去
- 应用场景:
- 保证了订单业务的不丢失:当订单处理系统消费异常时,会被放回到死信队列中
- 订单时效问题:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
2死信的三大来源
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basicReject 或 basicNack)并且 requeue=false(重入队参数).
3实战
结构图
基本结构代码
- 声明两大交换机
- 声明普通队列,并加入额外参数;
- 死信要到哪个死信交换机
- 列信交换机要发送到哪个routingKey
- 声明死信队列
- 进行绑定
/**
* 把该有的结构给定义出来,方便生产者和消费者直接调用
*/
public class Construction {
//定义各结构的名字
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String NORMAL_QUEUE = "normal_queue";
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//1、声明交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//2、声明普通队列。唯一不同的是,多了一个到死信队列的配置参数
//可以看到,到死信队列需要指明 交换机名称 和 绑定
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);//到哪个交换机
arguments.put("x-dead-letter-routing-key", "lisi");//交换机的哪个routinKey
/*
arguments.put("x-max-length", 6);//限制最大长度用的
arguments.put("x-message-ttl", 10000)//消息在队列的存活时间为10s
也可以在生产者发布消息的时候去定义:
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
*/
channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
//2、声明死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
//3、建立绑定
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
System.out.println("关系确定成功");
}
}
死信测试思路
1、消息 TTL 过期
-
可以声明队列是一个具有时效消息的队列:
arguments.put("x-message-ttl", 10000)
-
可以在发布的时候,定义消息的时效:
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
-
而后,模拟消费者宕机即可
2、队列达到最大长度
- 在声明普通队列时,加入额外参数:
arguments.put("x-max-length", 6);//限制最大长度用的
3、消息被拒绝(basicReject 或 basicNack)并且 requeue=false(重入队参数)
首先把消费者的自动应答关闭
channel.basicReject(long deliveryTag, boolean requeue)
, 重点是把requeue 设置成falsechannel.basicReject(long deliveryTag, boolean multiple, boolean requeue)
, 重点是把requeue 设置成false
七、延迟队列
1概念
- 延迟队列:故名思义,即在一个队列中,消息过了指定的时间才进行处理。即消息延迟多久被处理
- 在RabbitMQ中,没有特定的延迟队列的实现。延迟队列的实现是通过TTL的死信队列来实现的或者导入插件
2延迟队列使用场景
- 订单30分中内未付款则通知或自动取消该订单
3RabbitMQ中的TTL
消息设置TTL
rabbitTemplate.convertAndSend(exchange, queue, message, (correlationData) ->{
correlationData.getMessageProperties().setExpiration("10000");//源码中自己都写了:why not a Date or long?
return correlationData;
});
队列中设置TTL
//声明QB,并指定死信队列 及 过期时间
@Bean(NORMAL_QUEUE_QB)
public Queue QBQueue() {
return QueueBuilder
.durable(NORMAL_QUEUE_QB)
.deadLetterExchange(DEAD_EXCHANGE_Y)//也可以像原生的那样加arguments参数
.deadLetterRoutingKey("YD")
.ttl(20000)
.build();
}
两者的区别
- 设置了队列的TTL,消息一旦过期就会被丢弃(可以到死信队列)
- 消息中设置TTL,消息过期不一定被丢弃。消息是否过期是在即将投递到消费者之前判定的
- 不设置TTL,代表消息不会过期;TTL=0,代表如果消息不能马上被投递,则丢弃
整合SpringBoot
<!--RabbitMQ 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
5延迟队列实战
结构图
结构配置代码
@Configuration
public class TTLConfig {
//定义各结构的名字
public static final String NORMAL_EXCHANGE_X = "normal_exchange_x";
public static final String DEAD_EXCHANGE_Y = "dead_exchange_y";
public static final String NORMAL_QUEUE_QA = "normal_queue_qa";
public static final String NORMAL_QUEUE_QB = "normal_queue_qb";
public static final String DEAD_QUEUE_QD = "dead_queue_qd";
@Bean(NORMAL_EXCHANGE_X)
public DirectExchange XExchange() {
return ExchangeBuilder
.directExchange(NORMAL_EXCHANGE_X)
.build();
}
@Bean(DEAD_EXCHANGE_Y)
public DirectExchange YExchange() {
return ExchangeBuilder
.directExchange(DEAD_EXCHANGE_Y)
.build();
}
//声明QA,并指定死信队列 及 过期时间
@Bean(NORMAL_QUEUE_QA)
public Queue QAQueue() {
return QueueBuilder
.durable(NORMAL_QUEUE_QA)
.deadLetterExchange(DEAD_EXCHANGE_Y)
.deadLetterRoutingKey("YD")
.ttl(10000)
.build();
}
//声明QB,并指定死信队列 及 过期时间
@Bean(NORMAL_QUEUE_QB)
public Queue QBQueue() {
return QueueBuilder
.durable(NORMAL_QUEUE_QB)
.deadLetterExchange(DEAD_EXCHANGE_Y)//也可以像原生的那样加arguments参数
.deadLetterRoutingKey("YD")
.ttl(20000)
.build();
}
//声明QD,
@Bean(DEAD_QUEUE_QD)
public Queue QDQueue() {
return QueueBuilder.durable(DEAD_QUEUE_QD).build();
}
//以下是声明绑定
@Bean
public Binding QAToX(@Qualifier(NORMAL_QUEUE_QA) Queue QA,
@Qualifier(NORMAL_EXCHANGE_X) DirectExchange directExchange) {
return BindingBuilder
.bind(QA)
.to(directExchange)
.with("XA");
}
@Bean
public Binding QBToX(@Qualifier(NORMAL_QUEUE_QB) Queue QB,
@Qualifier(NORMAL_EXCHANGE_X) DirectExchange directExchange) {
return BindingBuilder
.bind(QB)
.to(directExchange)
.with("XB");
}
@Bean
public Binding QDToY(@Qualifier(DEAD_QUEUE_QD) Queue QD,
@Qualifier(DEAD_EXCHANGE_Y) DirectExchange directExchange) {
return BindingBuilder
.bind(QD)
.to(directExchange)
.with("YD");
}
}
生产者代码
@RestController
@RequestMapping("/ttl")
@Slf4j
public class TTLController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/test/{message}")
public String test(@PathVariable("message") String message) {
rabbitTemplate.convertAndSend(TTLConfig.NORMAL_EXCHANGE_X, "XA", "发送到QA:" + message);
rabbitTemplate.convertAndSend(TTLConfig.NORMAL_EXCHANGE_X, "XB", "发送到QB:" + message);
log.info("消息发送时间:{},内容:{}", new Date().toString(), message);
return message + "发送成功";
}
}
消费者代码
- 即监听一个队列
@Component
@Slf4j
public class TTLConsumer {
@RabbitListener(queues = TTLConfig.DEAD_QUEUE_QD)//监听队列QD
public void receiveQD(Message message) {
String msg = new String(message.getBody());
log.info("QD接受到的时间为:{},内容为:{}", new Date().toString(), msg);
}
}
测试结果
6延迟队列优化
- 背景:如果是基于队列的TTL,那么每增加一个时间需求,就要新增一个新的TTL队列
- 所以不对队列进行设置TTL,而对发布的消息设置TTL
- 消息设置TTL时延迟队列所存在的问题:
- 如果一个长一点TTL的消息先到达,而几乎相同时间内短的TTL消息后到达
- 那么,只有长一点的TTL消息会“堵住”队列的头,短的TTL消息并不会被队列优先考虑到
- 后果:长一点的TTL消息反而比短一点的TTL消息先被消费
7基于插件的延迟队列
原理
- 把消息过期时间的计算搬移到 交换机 中去
- 交换机优先分发TTL到期的消息
- 好处:正体现了交换机掌控分派消息的本质,不会出现以下基于死信队列的两个缺点:
- 队列中设置TTL:不能满足多TTL型的需求
- 消息中设置TTL:出现TTL短的消息不能被优先考虑
使用步骤
- 下载并安装相应类型的交换机插件
- 添加自定义交换机
- 指明自定义交换机的类型:x-delayed-message
- 指明交换机绑定类型(即四大类型中之一):args.put(“x-delayed-type”, “direct”);
- 而后和普通的交换机一样使用即可
// 自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
// 自定义交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
args);
}
8高级发布确认
1.发布确认Springboot版本
确认机制
- 当交换机收不到时,发送nack到生产者
- 当队列不存在时,将消息回退
架构图
(在下面的实战用到的)
配置文件详解
spring.rabbitmq.publisher-confirm-type=correlated
有三个参数:
- NONE
禁用发布确认模式,是默认值 - CORRELATED, 即原生的异步确认
发布消息成功到交换器后会触发回调方法 - SIMPLE, 即同步确认
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,(经过我的测试,不会。。)
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法
等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
2回退消息
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。
解决方法: rabbitTemplate.setMandatory(true);//设置队列收不到时的退回
交换机确认和回退消息实战
开启SptrinBoot配置
spring.rabbitmq.publisher-confirm-type=correlated #确认
spring.rabbitmq.publisher-returns=true #退回 经过测试,效果和相同rabbitTemplate.setMandatory(true);
配置类
定义结构
@Configuration
public class AdvancedPublisherConfig {
//交换机
public static final String EXCHANGE = "abc";
//队列
public static final String QUEUE = "abcd";
//routingKey
public static final String ROUTING_KEY = "adbde";
@Bean(EXCHANGE)
public DirectExchange exchange() {
return ExchangeBuilder
.directExchange(EXCHANGE)
.build();
}
@Bean(QUEUE)
public Queue queue() {
return QueueBuilder
.durable(QUEUE)
.build();
}
@Bean
public Binding binding(@Qualifier(QUEUE) Queue queue,
@Qualifier(EXCHANGE) DirectExchange directExchange) {
return BindingBuilder
.bind(queue)
.to(directExchange)
.with(ROUTING_KEY);
}
}
高级生产者
@Component
@Slf4j
//为了更体现通用性,可以另外起一个类专门实现RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback接口
public class AdvancedPublisher implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
RabbitTemplate rabbitTemplate;
public void publishMsg(String exchange, String routingKey, String message) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId("发布信息的id");
rabbitTemplate.convertAndSend(exchange,
routingKey,
message.getBytes(StandardCharsets.UTF_8),
correlationData);
System.out.println("已经发布消息:" + message);
}
/**
* 会在构造方法和init()方法之前执行
*/
@PostConstruct
public void setCallBackInterfaces() {
//通过源码可以看到,一个rabbitTemplate只能设置一个ConfirmCallback接口和ReturnCallback接口
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setMandatory(true);//设置队列收不到时的退回
rabbitTemplate.setReturnCallback(this);
}
/**
* 当数据不能到达交换机时,回调接口RabbitTemplate.ConfirmCallback
* @param correlationData 数据集
* @param ack 交换机是否已经进行了应答
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("交换机已经收到消息了,消息id={}", correlationData.getId());
} else {
log.info("消息id={},没有被接受到\n,原因为{}", correlationData.getId(), cause);
}
}
/**
* 当数据不能到达队列时,回调接口RabbitTemplate.ReturnCallback
* @param message 回退的消息
* @param replyCode 回退的代号
* @param replyText 回退的提示语
* @param exchange 回退的交换机
* @param routingKey 回退的routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("new String(message.getBody()) = " + new String(message.getBody(), StandardCharsets.UTF_8));
System.out.println("replyCode = " + replyCode);
System.out.println("replyText = " + replyText);
System.out.println("exchange = " + exchange);
System.out.println("routingKey = " + routingKey);
}
}
测试代码
@SpringBootTest
class SpringbootMqApplicationTests {
@Autowired
AdvancedPublisher advancedPublisher;
@Test
void contextLoads() {
advancedPublisher.publishMsg(AdvancedPublisherConfig.EXCHANGE, AdvancedPublisherConfig.ROUTING_KEY, "正常消息");
advancedPublisher.publishMsg("我是乱来的交换机", AdvancedPublisherConfig.ROUTING_KEY, "发送到不存在的交换机");
advancedPublisher.publishMsg(AdvancedPublisherConfig.EXCHANGE, "我是乱来的路由", "发送到不存在的队列");
}
}
结果
- 注意:发生消息回退时,会调用ConfimCallback接口。因为回退的原因是找不到队列,即一定有顺序到达了交换机
已经发布消息:正常消息, id = 1
交换机已经收到消息了,消息id=1
已经发布消息:发送到不存在的交换机, id = 2
消息id=2,没有被接受到,原因为channel error; protocol method: method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange '我是乱来的交换机' in vhost '/', class-id=60, method-id=40)
已经发布消息:发送到不存在的队列, id = 3
new String(message.getBody()) = 发送到不存在的队列
replyCode = 312
replyText = NO_ROUTE
exchange = abc
routingKey = 我是乱来的路由
交换机已经收到消息了,消息id=3 //注意这个,即使队列回退,也会回调ConfimCallback接口
如果是confim-type=simple
手动应答代码如下:
rabbitTemplate.invoke(operations -> {
rabbitTemplate.convertAndSend(
exchange,
routingKey,
message,
correlationData
);
return rabbitTemplate.waitForConfirms(2 * 1000);
});
4备份交换机
背景与概念
- 背景:当消息发生回退时,需要手动在生产者代码中处理回退的消息
- 解决方案:当消息不可路由时,由交换机转发给备份交换机处理
- mandatory 参数与备份交换机同时开启的时候,备份交换机优先级更高
- 应用场景:可以把回退的消息发到备份交换机,进行二次处理并报警
代码架构
- 在代码中,仅仅加一行:
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);// 设置该交换机的备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
ExchangeBuilder exchangeBuilder =
ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);// 设置该交换机的备份交换机
return (DirectExchange)exchangeBuilder.build();
}
9RabbitMQ其他知识点
1幂等性
- 多次点击一个服务,不会产生副作用,如付款按钮点击两次
- 在MQ中体现的就是:消息不能被重复消费
- RabbitMQ出现的问题:在消费者发送ack的时候,网络中断,导致MQ再次分发消息,导致消息二次消费
- 解决思路:
- 全局ID :为每个消息生成一个全局ID,如果已经被消费,则存储在数据库的主键中。其他消费者要消费前,先看一下数据库有无该主键
- redis:setnx
2优先级队列
-
在该队列中,消息附带着优先级,MQ会优先处理优先级高的队列
-
和原来普通的队列数据结构是不一样的
-
代码:
//声明优先级队列 params.put("x-max-priority", 10); channel.queueDeclare(QUEUE_NAME, true, false, false, params); //发送带有优先级的消息 //如果不设置,则是使用默认优先级,即最低优先级 AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build(); channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
3惰性队列
-
概念:将消息尽可能存入到磁盘中的队列,在要消费时才把消息加载到内存
-
设计目标:防止消息堆积,让队列容纳更多的消息
-
与持久化的区别:持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份
-
缺点:当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息 -
代码:
Map<String, Object> args = new HashMap<String, Object>(); args.put("x-queue-mode", "lazy"); channel.queueDeclare("myqueue", false, false, false, args);
10RabbitMQ 集群
暂时用不到,记录下知识点:
- 配置集群
- 镜像队列:集群中的服务器中的队列是有多份的
- 集群配合nginx可以实现高可用负载均衡
- Federation Exchange/Queue:一种同步机制
- Shovel:一种同步机制