RabbitMQ详解
1. RabbitMQ简介
RabbitMQ是由erlang语言开发,基于AMQP(高级消息队列协议)
协议实现的消息队列,它是一种应用程序之间的通信方法。
RabbitMQ官方地址:http://www.rabbitmq.com
1.1 什么是消息队列
MQ:全称Message Queue,即消息队列。
消息队列:是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者设计模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。
1.2 消息队列使用场景与优势【异步,解耦,削峰】
- 异步:高并发环境下,由于来不及同步处理,请求往往会发生堵塞。我们可以异步处理请求,从而缓解系统的压力。将不需要同步处理的并且耗时长的操作,由消息队列通知消息接收方进行异步处理。
场景:
用户注册后,需要发送注册邮件和注册成功短信通知【这两个操作都不是必须的,只是一个通知,没必要让用户等待收到邮箱和短信后才算注册成功】。
引入消息队列后,将发送邮件和短信的业务交给消息队列,实现异步处理。
# 个人感觉使用MQ主要作用就是异步处理,主要用于一些耗时的任务。 其实使用多线程也可以解决的,而且多线程使用起来相比MQ可方便得多。但是多线程只能用于单体项目。
# MQ主要是比多线程有更好的可靠性,可扩展性。
消息持久化:使用RabbitMQ可以将消息持久化到磁盘上,即使在应用崩溃或者断电的情况下,也不会丢失消息。
可靠性:RabbitMQ通过各种机制确保消息的可靠传输,例如生产者确认、消费者确认、持久化等,确保消息不会被丢失或重复传输。
可扩展性:RabbitMQ可以通过添加更多的节点来实现更高的吞吐量和更好的容错性,同时也可以通过添加更多的队列来实现更好的任务分离和负载均衡。
总的来说,使用RabbitMQ可以提供更高的可靠性、可扩展性和消息持久化等优势,这些是使用多线程无法轻易实现的。
当然,在某些情况下使用多线程也是可行的,具体要看具体场景和需求。
- 解耦:MQ相当于一个中介,生产方通过MQ与消费方交互,不直接进行交互,它将生产方与消费方进行解耦合,不至于当时功能里面的某一个操作因为宕机了导致后续操作无法进行。
场景:
双十一购物狂欢节,用户下单后,订单系统 需要通知 库存系统 减少响应库存量,若 库存系统 出现故障,此笔订单就不能成功。
引入消息队列后,订单系统向消息队列发送用户下单的消息,从而与消费者进行了解耦,消息队列会将数据持久化【防止rabbitMQ宕机后消息丢失或库存系统宕机没消费消息】,库存系统监听消息队列的消息。
- 削峰:
场景:
秒杀活动,一般流量会很大,可能导致某个系统直接扛不住而挂掉。
引入消息队列,用户发起请求时,先来到消息队列再去秒杀系统。在消息队列中对消息进行处理(比如请求量达到消息队列阈值时,直接抛弃那些请求或跳转错误页面),如此一来可缓解因高并发请求所导致秒杀系统扛不住挂掉的问题。
1.3 消息队列的劣势
- 系统可用性降低:
系统依赖了MQ,若MQ宕机,就会对业务造成影响。要考虑如何保证MQ的高可用。
- 系统复杂度提高:
使用MQ进行异步调用后,如何保证消息没有被1、MQ重复消费?
2、保证消息可靠性?
3、怎么保证消息传递的顺序性?
4、消息积压问题?
- 一致性问题:
A系统处理完业务,通过MQ给 B,C,D 三个系统发消息数据,若B,C系统处理成功,D系统处理失败,消息被如何多个消费者消费时的事务问题?
1.4 想使用MQ,你的功能需要满足什么条件
1. 生产者不需要从消费者处获得反馈就能完成该功能的处理。
2. 容许短暂的不一致性【消息的传递可能会受到网络延迟、机器故障或其他因素的影响,从而导致消息的顺序或者到达时间发生变化。】
# 加入了MQ,帮项目提升了些效果,但是管理MQ这些成本远超提升的这些效果,就不适合MQ了。
# 分布式项目(你的应用需要在不同的组件之间进行异步通信),如果是单体项目就使用消息队列可能有点过度设计。
# 总结:使用消息队列需要考虑系统的复杂性、异步通信的需求、高可用性、消息的可靠性和增量扩展等方面的因素,才能真正发挥消息队列的作用。
2. 为什么要学习RabbitMQ
在电子商务平台中处理订单和库存管理。假设你正在开发一个电子商务网站,以下是一个使用 RabbitMQ 的场景:
1. 用户下单购买商品后,系统需要及时更新库存信息并处理订单。
2. 同时,系统还需要发送订单确认邮件给用户,以及触发其他后续操作(如支付处理、物流安排等)。
使用消息队列来解决:
1. 当用户下单购买商品时,系统将订单信息发送到订单消息队列去。 这样可以简化订单处理系统的实现,避免直接在订单提交过程中进行耗时的数据库操作,提高系统的性能和并发处理能力。
2. 商品库存服务需要更新商品库存信息。系统将订单信息发送到库存消息队列去。 这样可以简化库存管理系统的实现,使其能够实时响应订单变化,同时保持库存数据的一致性。
2. 当订单信息被处理后,邮件服务可以从消息队列中获取订单信息,发送邮件给用户。 这样可以简化邮件通知系统的实现,使其与订单处理系统解耦,提高系统的可维护性和可扩展性。
3. 常见MQ产品
- ActiveMQ:基于JMS
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
- Kafka:分布式消息系统,高吞吐量
4. RabbitMQ架构
4.1 简单架构图
4.2 完整架构图
RabbitMQ中有多个Virtual Host。
4.3 交换机类型
在RabbitMQ中有五种交换机类型。
- default
- fanout Publish/Subscribe
- direct Routing
- topic Topics
- headers
5. RabbitMQ的通讯方式
6. 用Java方式操作RabbitMQ
6.1 获取RabbitMQ的连接
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
public static Connection getConnection() {
// 创建Connection工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("47.114.186.28");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/test");
// 创建Connection
Connection conn = null;
try {
conn = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
// 返回
return conn;
}
@Test
public void getConnection() throws IOException {
Connection connection = RabbitMQClient.getConnection();
connection.close();
}
6.2 RabbitMQ的“Hello World”通讯方式
生产者
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel通道
Channel channel = connection.createChannel();
//3. 发布消息到exchange,发布的同时需要指定路由的规则
// 参数1:- 指定exchange 如果使用""【表示使用默认exchange】。
// 参数2:- 指定路由规则 【是一个字符串类型的参数。路由键用于将消息路由到指定的队列中。如果消息发送到的是默认的交换机,路由键应该是目标队列的名称。】。
// 参数3:- 指定传递的消息所携带的properties 【即:传递的消息的额外设置,可以设置各种属性,如消息的优先级、过期时间等。】,没有就写null。
// 参数4:- 指定发布的具体消息内容 byte[]类型
String msg = "Hello-World!";
channel.basicPublish("","HelloWorld",null,msg.getBytes());
// Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。发布者是和交换机打交道的,所以这里不能帮助实现持久化本地
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
消费者
public class Consumer {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明[创建]队列-HelloWorld
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用】【当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 指定当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
channel.queueDeclare("HelloWorld", true, false, false, null);
DefaultConsumer consume = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收到消息:" + new String(body, "UTF-8"));
}
};
//4. 开启监听队列
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true:接收到消息后,会立即自动告诉RabbitMQ消息已消费了)
//参数3:consumer - 指定消费时的消费回调(收到消息后,消费者要干点什么事)
channel.basicConsume("HelloWorld", true, consume); // 这个才是开启监听的方法
System.out.println("消费者开始监听队列!");
// 可以实现输入字符 , 用来将程序在这里等着,相当于debug
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
6.3 RabbitMQ的“Work”通讯方式
“Hello world”那种通讯方式存在一个弊端,若消费者消费消息的速度很慢,可能会导致生产者发布的消息形成堆积。
我们接下来介绍一种通讯方式,就是一个生产者发布消息,有多个消费者监听,谁收到消息,谁就消费【该通讯方式中,消息一旦被消费了,就消失。所以不会出现重复消费的情况】。这样就解决了上述所说的问题。
public class Consumer1 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
final Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用】【当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 指定当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
channel.queueDeclare("Work", true, false, false, null);
//3.1 默认情况下是平均消费,有10个消息,那么2个消费者各消费5个消息。 如果想指定当前消费者一次消费多少个消息,可通过basicQos设置。
//【【如果要想让某个消费能力强的消费者消费更多的消息,就可以指定该消费者的消费能力。PS: 此时必须改为手动ACK】】
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1号接收到消息:" + new String(body, "UTF-8"));
// 【【手动ack 表示我已经消费完了】】
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//4. 开启监听Queue
// 将自动消费改为false 表示手动消费
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true:接收到消息后,会立即自动告诉RabbitMQ消息已消费了)
//参数3:consumer - 指定消费时的消费回调(收到消息后,消费者要干点什么事)
channel.basicConsume("Work", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Consumer2 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
final Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("Work", true, false, false, null);
//3.1 指定当前消费者,一次消费多少个消息。
channel.basicQos(1);
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者2号接收到消息:" + new String(body, "UTF-8"));
// 手动ack,消费完了告诉rabbitMQ我消费完了
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("Work", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel通道
Channel channel = connection.createChannel();
//3. 发布消息到exchange,同时指定路由的规则
for (int i = 0; i < 10; i++) {
// 参数1:- 指定exchange 如果使用""【表示使用默认exchange】。
// 参数2:- 指定路由规则 【是一个字符串类型的参数。路由键用于将消息路由到指定的队列中。如果消息发送到的是默认的交换机,路由键应该是目标队列的名称。】。
// 参数3:- 指定传递的消息所携带的properties 【即:传递的消息的额外设置,可以设置各种属性,如消息的优先级、过期时间等。】,没有就写null。
// 参数4:- 指定发布的具体消息内容 byte[]类型
String msg = "Hello-World!" + i;
channel.basicPublish("", "Work", null, msg.getBytes());
}
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
6.4 RabbitMQ的“Publish/Subscribe”通讯方式 (广播通讯方式)
public class Consumer1 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列要消费的队列:pubsub-queue1
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用】【当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 指定当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
channel.queueDeclare("pubsub-queue1", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1号接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//4. 开启监听Queue
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true:接收到消息后,会立即自动告诉RabbitMQ消息已消费了)
//参数3:consumer - 指定消费时的消费回调(收到消息后,消费者要干点什么事)
channel.basicConsume("pubsub-queue1", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Consumer2 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列要消费的队列:pubsub-queue1
channel.queueDeclare("pubsub-queue2", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1号接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("pubsub-queue2", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 创建exchange
//参数1: exchange的名称 若该交换机不存在,则创建
//参数2: 指定exchange的类型 常用的有默认的,direct,fanout,topic。前面2种我们都是使用的默认的交换机。 FANOUT:Publish/Subscribe通讯方式 DIRECT:Routing通讯方式 TOPIC:Topic通讯方式
channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT);
//3.1 exchange绑定某一个队列 该操作可以在生产者干 也 可以在消费者干
//参数1:指定队列
//参数2:指定交换机
//参数3:指定routingKey规则
channel.queueBind("pubsub-queue1", "pubsub-exchange", "");
channel.queueBind("pubsub-queue2", "pubsub-exchange", "");
//4. 发布消息到exchange,同时指定路由的规则
for (int i = 0; i < 10; i++) {
String msg = "Hello-World!" + i;
// 注意这里,之前发布消息采用默认的交换机 我们使用的是 ""。 现在我们要指定交换机了。
// 参数1:- 指定exchange 如果使用""【表示使用默认exchange】。
// 参数2:- 指定路由规则 【是一个字符串类型的参数。路由键用于将消息路由到指定的队列中。如果消息发送到的是默认的交换机,路由键应该是目标队列的名称。】。
// 参数3:- 指定传递的消息所携带的properties 【即:传递的消息的额外设置,可以设置各种属性,如消息的优先级、过期时间等。】,没有就写null。
// 参数4:- 指定发布的具体消息内容 byte[]类型
channel.basicPublish("pubsub-exchange", "", null, msg.getBytes()); // 表示绑定到pubsub-exchange交换机,该交换机有两个队列:pubsub-queue1和pubsub-queue2
}
System.out.println("生产者发布消息成功!");
//5. 释放资源
channel.close();
connection.close();
}
}
6.5 RabbitMQ的“Routing”通讯方式
和publish的区别就是:不再是直接与队列绑定了,而是通过指定了 routingKey 去绑定到队列。
public class Consumer1 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用】【当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 指定当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
channel.queueDeclare("routing-queue-error", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者ERROR接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//4. 开启监听Queue
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true:接收到消息后,会立即自动告诉RabbitMQ消息已消费了)
//参数3:consumer - 指定消费时的消费回调(收到消息后,消费者要干点什么事)
channel.basicConsume("routing-queue-error", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Consumer2 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("routing-queue-info", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者INFO接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("routing-queue-info", false, consumer);
System.out.println("开始消费消息。。。。");
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 创建exchange, 绑定队列:routing-queue-error 和 routing-queue-info
//参数1: exchange的名称 若该交换机不存在,则创建
//参数2: 指定exchange的类型 常用的有默认的,direct,fanout,topic。前面2种我们都是使用的默认的交换机。 FANOUT:Publish/Subscribe通讯方式 DIRECT:Routing通讯方式 TOPIC:Topic通讯方式
channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT);
//3.1 exchange绑定某一个队列 该操作可以在生产者干 也 可以在消费者干
//参数1:指定队列
//参数2:指定交换机
//参数3:指定routingKey规则
channel.queueBind("routing-queue-error", "routing-exchange", "ERROR");
channel.queueBind("routing-queue-info", "routing-exchange", "INFO");
//4. 发布消息到exchange,同时指定路由的规则
// 参数1:指定exchange,如果使用""【表示使用默认exchange】。
// 参数2:指定路由规则【“Hello World”通讯方式 直接使用具体的队列名称即可】。
// 参数3:指定传递的消息所携带的properties【即:传递的消息的额外设置】,没有就写null。
// 参数4:指定发布的具体消息内容,byte[]类型
channel.basicPublish("routing-exchange", "ERROR", null, "ERROR".getBytes());// 发布到"routing-exchange",并且要求该交换机的队列的routingKey是 ERROR
channel.basicPublish("routing-exchange", "INFO", null, "INFO-1".getBytes());
channel.basicPublish("routing-exchange", "INFO", null, "INFO-2".getBytes());
channel.basicPublish("routing-exchange", "INFO", null, "INFO-3".getBytes());
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
6.6 RabbitMQ的“Topic”通讯方式
和topic的区别就是这里的 routingKey 有所不同。
public class Consumer1 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("topic-queue-1", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("对红色动物感兴趣接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("topic-queue-1", false, consumer);
System.out.println("开始消费消息。。。。");
// System.in.read();
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Consumer2 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("topic-queue-2", true, false, false, null);
//3.5 指定当前消费者,一次消费多少个消息
channel.basicQos(1);
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("对快的和兔子感兴趣接收到消息:" + new String(body, "UTF-8"));
// 手动ack
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("topic-queue-2", false, consumer);
System.out.println("开始消费消息。。。。");
// System.in.read();
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
}
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
// 3. 创建exchange绑定队列 topic-queue-1 topic-queue-2
//参数1: exchange的名称 若该交换机不存在,则创建
//参数2: 指定exchange的类型 常用的有默认的,direct,fanout,topic。前面2种我们都是使用的默认的交换机。 FANOUT:Publish/Subscribe通讯方式 DIRECT:Routing通讯方式 TOPIC:Topic通讯方式
channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC);
//3.1 exchange绑定某一个队列 该操作可以在生产者干 也 可以在消费者干
//参数1:指定队列
//参数2:指定交换机
//参数3:指定routingKey规则
// 该模式与routing模式的区别就在于 他们的routingKey有所不同
// 需求:我们要发布动物的信息,该动物有3个属性 <speed>.<color>.<what>
// *.red.* -> *占位符 即:一个*代表一个内容
// fast.# -> #通配符 即:一个#代表多个内容
// 如: *.*.rabbit 等同于 #.rabbit
channel.queueBind("topic-queue-1", "topic-exchange", "*.red.*"); // topic-queue-1队列只对红色动物感兴趣
channel.queueBind("topic-queue-2", "topic-exchange", "fast.#");
channel.queueBind("topic-queue-2", "topic-exchange", "*.*.rabbit"); // topic-queue-2队列对快的动物感兴趣 和 对兔子感兴趣。 注意: 一个队列可以被多次绑定
//4. 发布消息到exchange,同时指定路由的规则
// 参数1:- 指定exchange 如果使用""【表示使用默认exchange】。
// 参数2:- 指定路由规则 【是一个字符串类型的参数。路由键用于将消息路由到指定的队列中。如果消息发送到的是默认的交换机,路由键应该是目标队列的名称。】。
// 参数3:- 指定传递的消息所携带的properties 【即:传递的消息的额外设置,可以设置各种属性,如消息的优先级、过期时间等。】,没有就写null。
// 参数4:- 指定发布的具体消息内容 byte[]类型
channel.basicPublish("topic-exchange", "fast.red.monkey", null, "红快猴子".getBytes());
channel.basicPublish("topic-exchange", "slow.black.dog", null, "黑漫狗".getBytes());
channel.basicPublish("topic-exchange", "fast.white.cat", null, "快白猫".getBytes());
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
7. SpringBoot整合RabbitMQ[基于配置类]
整合各种模式可以参考:https://juejin.cn/post/6976033887449251876
7.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
7.2 编写配置文件
spring:
rabbitmq:
host: 47.114.216.28
port: 5672
username: admin
password: admin
virtual-host: test
7.3 创建配置类 —— 声明exchange,queue 并将其绑定
// rabbitMQ的配置类可以声明在生产者端,也可以声明在消费者端 都可以
@Configuration
public class RabbitMQConfig {
//1. 创建exchange 以topic通讯方式为例,因为其最灵活
@Bean
public TopicExchange getTopicExchange(){
// 参数1(String name):交换机名称
// 参数2(boolean durable):交换机是否持久化。如果为true,则RabbitMQ服务器在重启后仍然存在。如果为false,则在RabbitMQ服务器重启后将被删除。
// 参数3(boolean autoDelete):交换机是否自动删除。如果为true,表示当该交换机不再被任何队列或者交换机所绑定时,会自动被删除
// 参数4(Map<String, Object> arg):声明交换机的参数
return new TopicExchange("boot-topic-exchange",true,false);
}
//2. 创建queue
@Bean
public Queue getQueue(){
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用,当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 声明当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
return new Queue("boot-queue",true,false,false,null);
}
//3. 创建Binding
@Bean
public Binding getBinding(TopicExchange topicExchange,Queue queue){
// .bind() 绑定哪个队列 .to() 到哪个交换机 .with() 指定routingKey
return BindingBuilder.bind(getQueue()).to(getTopicExchange()).with("*.red.*");
}
}
// 上述写法可以写成下面这种=================================================================================
@Configuration
public class RabbitMQConfig {
public static final String TOPIC_NAME = "topicExchange";
public static final String QUEUE_NAME = "topicQueue";
//1. 创建exchange 以topic通讯方式为例,因为其最灵活
@Bean("exchange")
public Exchange getTopicExchange(){
return ExchangeBuilder.topicExchange(TOPIC_NAME).durable(true).build(); // 它是这种构建者的方式构建来指定
}
//2. 创建queue
@Bean("queue")
public Queue getQueue(){
return QueueBuilder.durable(QUEUE_NAME).build(); // 他也是这种构建者
}
//3. 创建Binding
@Bean
public Binding getBinding(@Qualifier("exchange")Exchange exchange,@Qualifier("queue")Queue queue){ // 这个配置类可能又很多交换机和配置类,所以一般都会用 @Bean注解指定它的名称。而这里在绑定的时候,也要区分不同的交换机与队列.
// .bind() 绑定哪个队列 .to() 到哪个交换机 .with() 指定routingKey .noargs() 不需要指定参数-如果不写这个也代表不需要指定参数
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*").noargs();
}
}
7.4 发布者发布消息到rabbitMQ
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
// 参数1(String exchange):指定交换机
// 参数2(String routingKey):指定routingKey
// 参数3(Object message):指定发送的数据内容
// 参数4(MessagePostProcessor m):用于修改消息的属性
// void convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor m)
// 如果使用 convertAndSend 方法发送消息时指定的交换机不存在,或者消息发送失败,会抛出 AmqpException 异常。因此,建议在使用 convertAndSend 方法发送消息时进行异常处理。
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
}
7.5 消费者监听rabbitMQ接收消息
// 编写监听类
@Component
public class Consumer {
@RabbitListener 注解有以下常用属性:
id:指定监听器的 ID,可以用于唯一标识一个监听器。
containerFactory:指定 RabbitListenerContainerFactory 的名称,用于创建消息监听容器。
queues:指定监听的队列名称或队列对象,可以是一个字符串数组或 Queue 对象数组。
exclusive:指定是否独占队列,默认为 false。
priority:指定消费者的优先级,优先级越高的消费者优先处理消息。
group:指定消费者组的名称,用于多个消费者共同消费同一个队列时进行分组。
concurrency:指定消费者的并发数,默认为 1,即每次只能处理一个消息。
autoStartup:指定监听器是否自动启动,默认为 true。
ackMode:指定消息的确认模式,可以是 AcknowledgeMode.AUTO、AcknowledgeMode.MANUAL 或 AcknowledgeMode.NONE。
@RabbitListener(queues = "boot-queue") // 指定要监听的队列
public void getMessage(Message message) throws IOException { // 将来来消息了,就会被这个Message对象接收。
System.out.println("接收到消息:" + message);
}
//使用 @RabbitListener 注解标记的方法可以接受多种类型的参数,包括:
byte[] 或 String:用于接收消息的原始字节数组或字符串形式。
org.springframework.messaging.Message或其子类:表示经过消息转换后的消息对象,其中包含了消息的 payload 和 headers。
org.springframework.amqp.core.Message或其子类:表示接收到的原始消息对象。
自定义 POJO 类型:可以根据需要自定义一个 POJO 类型,用于接收消息的内容。
Channel: 它是 RabbitMQ 客户端与服务器之间的通信信道
}
8. 思考几个问题
思考几个问题?
- 生产者在发布消息到RabbitMQ的交换机时,由于网络问题,导致没有真发送成功到交换机,消息会丢失吗? confirm机制
会,生产者执行了发布消息的方法,就会认为已经发布过去了。可利用 Confirm(确认)机制 实现或利用其提供的 事务操作机制【影响效率,这里不介绍它】。 PS:Confirm机制是保证了消息发送到Exchange上。而消费者监听的不是Exchange,而是队列。
- 生产者成功发布消息到交换机了,但是交换机分发消息到队列的时候出现了问题,导致没有真分发成功,消息会丢失吗? return机制
会,消费者是与队列交互的,如果消息没有分发到队列,队列就没有消息。可利用Return机制实现。 PS:Return机制是保证Exchange的消息分发到队列。
- 如果消息已经到达了RabbitMQ,还没发送给消费者时,RabbitMQ宕机了,消息会丢失吗? 持久化机制
会,RabbitMQ的队列提供了持久化机制,若消息到了RabbitMQ已经到了队列那里了,就能持久化。当RabbitMQ重连的时候消息就能发送给消费者了。
- 消费者在消费消息的时候,还没消费完,此时消费者宕机了,消息会丢失吗? 手动ack
会,RabbitMQ提供了手动ACK。当成功消费完消息的时候再手动ACK告诉生产者我消费完了。
9. confirm机制与return机制
9.2 开启confirm机制与return机制
9.2.1 SpringBoot方式实现RabbitMQ的confirm与return机制
# 1. 配置文件开启confirm与return机制 通常是在生产方
spring:
rabbitmq:
host: 47.144.116.28
port: 5672
username: admin
password: admin
virtual-host: test
publisher-confirm-type: simple # 开启confirm机制
publisher-returns: true # 开启return机制
# 关于publisher-confirm-type的取值
NONE: 禁用发布确认模式,是默认值
CORRELATED: 发布消息成功到交换器后会触发回调方法
SIMPLE: 经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待
broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker。
// 2. 创建关于 SpringBoot 实现可靠性 的配置类[return与confirm的配置类]
@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct // 表示在初始化PublisherConfirmAndReturnConfig对象时,会执行该方法。
public void initMethod(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
// 基于confirm的回调 此方法用于监听消息是否发送到交换机
// correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
// ack:消息投递到broker 的状态,true表示成功。
// cause:表示投递失败的原因
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息已经送达到Exchange");
}else{
System.out.println("消息没有送达到Exchange");
}
}
// 基于return的回调 此方法用于监听消息是否发送到队列 注意:消息没有送达队列时该方法才会执行
// message:无法路由到目标队列的消息对象。
// replyCode:返回码,指示无法路由到目标队列的原因。
// replyText:返回信息,包含返回码的解释。
// exchange:发送消息时指定的交换机名称。
// routingKey:发送消息时指定的路由键。
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息没有送达到Queue");
}
}
生产者发布消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
消费者消费消息
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Message message, Channel channel,String msg) throws IOException {
System.out.println("接收到消息:" + message);
}
}
10. 持久化
持久化可以提高 RabbitMQ 的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。
持久化可分为以下几种情况:
- 交换机的持久化
- 队列的持久化
- 消息的持久化
10.1 交换机的持久化
交换器的持久化是在声明交换器的时候,将 durable 属性设置为 true。如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换机就会被删除。
交换机的持久化:是保证重启后交换机不丢失, 交换机并没有持久化消息的功能。
//原生Api
/**
* 参数1:交换机名称
* 参数2:交换机类型
* 参数3:是否持久化 默认 false
*/
channel.exchangeDeclare("logs_direct", BuiltinExchangeType.DIRECT,true);
// springboot方式
@Bean
public TopicExchange payTopicExchange(){
// 参数1(String name):交换机名称
// 参数2(boolean durable):交换机是否持久化。如果为true,则RabbitMQ服务器在重启后仍然存在。如果为false,则在RabbitMQ服务器重启后将被删除。
// 参数3(boolean autoDelete):交换机是否自动删除。如果为true,表示当该交换机不再被任何队列或者交换机所绑定时,会自动被删除
// 参数4(Map<String, Object> arg):声明交换机的参数
return new TopicExchange(exchangeMame,true,false);
}
10.2 队列的持久化
队列的持久化也是在声明队列的时候,将durable参数设置为true。如果队列不设置持久化,那么 RabbitMQ服务重启之后,队列就会被删除,既然队列都不存在了,队列中的消息也会丢失。
// 原生Api
/**
* 参数1:String queue 队列名称 如果队列不存在会自动创建
* 参数2:boolean durable 队列是否持久化 true 持久化 false 不持久化 默认:false
* 参数3:boolean exclusive 是否独占队列 true 独占队列 false 不独占 默认:true
* 参数4:boolean autoDelete 是否在消费完成后自动删除 true 自动删除 默认:true
* 参数5:Map<String, Object> arguments 额外附加参数
*/
channel.queueDeclare("hello-1",true,false,false,null);
@Bean
public Queue dlQueue(){
//参数1:queue - 指定队列的名称 【若该队列不存在,则自动创建】
//参数2:durable - 当前队列是否需要持久化 【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
//参数3:exclusive - 是否独占 【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用,当连接对象Connection被close()之后,当前队列会自动删除】
//参数4:autoDelete - 是否自动删除 【表示当所有的消费者连接关闭后,是否自动删除该队列】
//参数5:arguments - 声明当前队列的其他信息 【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
return new Queue(dlQueue,true);
}
10.3 消息的持久化
队列的持久化能保证其本身不会因重启、关闭、宕机的情况而丢失,但是并不能保证其内部所存储的消息不会丢失。
要确保消息不会丢失,需要将消息设置为持久化。
在发送消息的时候,通过将BasicProperties中的属性deliveryMode(投递模式)设置为 2 即可实现消息的持久化。
// 原生Api
channel.basicPublish("exchangeName" , "routingKey",
new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.build(),
"ddf".getBytes());
// springboot方式
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(String message) {
rabbitTemplate.convertAndSend("exchangeName", "routingKey", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}
});
}
可以将所有的消息都设置为持久化,但是这样会严重影响 RabbitMQ 的性能。写入磁盘的速度比写入内存的速度慢得不只一点点。
对于可靠性不是那么高的消息可以不采用持久化处理,以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做权衡。
队列的持久化,消息本身的持久化 ———————————————— 才是保证消息在MQ中不丢失的2种方式
一般的系统也用不到对消息进行持久化。不过交换机和队列的持久化还是要支持的。
上述几种方式我们只是保证了消息发送到交换机、队列时不会由于RabbitMQ的重启、关闭、宕机的情况而丢失消息。但如果消费者在消费的时候出现问题了呢?
对于消费者来说,如果在订阅消息的时候,将autoAck设置为了true(即:自动ack,一收到消息就确认,不管是否执行成功),消费者接收到相关消息后,还没有正式处理消息逻辑之前,就出现了异常挂掉了,但消息已经被自动确认了,这样也算数据丢失。
对此有如下几种方式解决:
1. 可以用手动Ack,消费者成功消费后告诉mq我成功消费了。
2. 将消息重试并设置死信队列
11. SpringBoot实现手动ACK
RabbitMQ的确认机制是自动确认的,消费者收到消息后立马确认。
自动确认:可能出现消费者最后没有成功消费信息的可能,所以我们一般需要手动确认(通过调用 channel.basicAck()),即在成功消费后再告诉MQ。
如果消费者在消费过程中,出现了异常,我们可以捕获异常后调用 basicNack或basicReject绝消息,让MQ重新发送。
11.1 更改配置文件 通常是在消费方
在上述操作基础上更改
spring:
rabbitmq:
host: 47.174.116.28
port: 5672
username: admin
password: admin
virtual-host: test
listener:
simple:
acknowledge-mode: manual # auto:自动(默认) manual:表示手动ACK none:表示不对消息进行确认,也不对消息进行拒绝
11.2 更改消费者
开启手动ack后,如果消费了消息后,只要消费者不ack,则在MQ中会显示该消息未被消费。
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Message message, Channel channel,String msg) throws IOException {
(确认消息)
// deliveryTag :消息的编号 【需要通过: message.getMessageProperties().getDeliveryTag() 获取】
// multiple:是否批量进行签收。
// true: 批量签收所有消息。
// false:表示不批量签收,签收该消息编号的消息
void basicAck(long deliveryTag, boolean multiple) throws IOException; 用于确认当前消息
(拒绝消息)
// deliveryTag:消息的编号
// multiple:是否批量拒绝。
// true: 批量拒绝所有消息。
// false:表示不批量拒绝,拒绝该消息编号的消息
// requeue:
// true:会重新将这条消息放回队列并重新发送该消息
// false:会立即把消息从队列中移除。
// 如:channel.BasicNack(3, true, false); 第一个参数编号中如果输入3,则消息DeliveryTag小于等于3的,这个Channel的,都会被拒收
void basicNack(long deliveryTag, boolean multiple, boolean requeue); 用于拒绝当前消息 // ############【可以一次性拒绝多个】############
(拒绝消息)
// deliveryTag: 消息的编号
// requeue:
// true:会重新将这条消息放回队列并重新发送该消息
// false:会立即把消息从队列中移除。
void basicReject(long deliveryTag, boolean requeue); 用于拒绝当前消息 // ############【只能一次性拒绝1个】############
// 注意:basicReject方法一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用 basicNack 这个方法。
// ★★★★★★★★★尤其注意:如果消息被你拒绝,且你设置了 requeue 为true,则消息会回到队列重新发送给其他消费者(多个的时候,一个的时候就只发给原先发送的那个消费者),若再次发送消息,代码逻辑还是拒绝,则消息会不断一直不停的重新发送。形成了‘死锁’。★★★★★★★
// ★★★★★★★★★所以建议别将requeue设置为true,设置称false,并为其设置死信队列,防止消息丢失。如果你的业务需求就是明确的要拒绝该消息,那就直接拒绝,不用设置死心交换机★★★★★★★★★
System.out.println("接收到消息:" + message);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
12. 如何对消息设置过期时间
在RabbitMQ中分别可以对 消息,队列 设置过期时间【这里的过期时间指:消息等待消费者消费的等待时间,而不是包含被消费者消费过程中所消耗的时间】
注意:消息到期后(如果是通过队列设置过期时间的消息到期,就会变成“死信”,而如果是消息本身设置的过期,到期后不会变成‘死信’)。
注:如果队列设置了死信队列,那么这条“死信”消息就会被转发到死信队列上,该消息就可以被正常消费。
注:若同时设置过期时间,谁先过期,先采用谁的。
住:若是通过队列设置的过期时间,则到期后,会立马删除,若有死信的话,会变成死信。
若是通过消息属性设置的过期时间,到期后,可能不会立马删除,他底层实现是在消息即将投递到消费者之前判定的是否过期的,过期才删除
12.1 通过队列属性设置队列中消息过期时间
@Bean
public Queue payQueue(){
Map<String,Object> params = new HashMap<>();
//设置队列的过期时间
params.put("x-message-ttl",10000);
//也可以使用下面这种写法
//QueueBuilder.durable("ttl-quequ").ttl(10000).build();
return QueueBuilder.durable("ttl-quequ").withArguments(params).build(); // 构建者那种方式
}
// 不采用上述那种构建者那种方式
@Bean
public Queue getQueue(){
Map<String,Object> params = new HashMap<>();
//设置队列的过期时间
params.put("x-message-ttl",10000);
// 参数1:队列名称 参数2:是否持久化 参数3:是否独占 参数4:是否自动删除 参数5:指定当前队列的其他信息
return new Queue("boot-queue",true,false,false,params);
}
// 此时,若该队列收到消息后,10秒内没有被消费者消费,则该消息就会从队列中被删除消失。
12.2 通过消息本身进行单独设置
// 在发送消息的时候设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
@Overeide
public Message postProcessMessage(Message message) throws AmqpException{
message.getMessageProperties().setExpiration("30000");
return message;
}
};
rabbitTemplate.convertAndSend("exchangeName","routingKey","消息内容",messagePostProcessor); // 指定过期时间,需要MessagePostProcessor对象
// 【如果为消息本身设置了过期时间,当消息过期时。需要注意的是,RabbitMQ 并不会在每个消息的 TTL 到期时立即将其删除,而是在检查队列中的消息时,将所有已过期的消息标记为过期状态,并从队列中删除它们。】
13. 如何设置队列最长长度
// 设置了最大长度后,当超出的消息,就会被丢失,如果设置了死心队列,则多的消息会被转发到死心队列。如最大长度为5,发了11条信息,则后6条会被转到死心队列去。
@Bean
public Queue getQueue(){
Map<String,Object> params = new HashMap<>();
// 设置队列的过期时间
params.put("x-max-length",5);
// 参数1:队列名称 参数2:是否持久化 参数3:是否独占 参数4:是否自动删除 参数5:指定当前队列的其他信息
return new Queue("boot-queue",true,false,false,params);
}
14. 什么是死信交换机与死信队列
出处:https://juejin.cn/post/6976778266472366087
DLX ,全称为 Dead-Letter-Exchange ,可以称之为死信交换机。
它其实也是一个正常的交换机,和一般交换机没什么区别,它能在任何的队列上被指定。实现也很简单,实际上就是设置某个队列的属性。
当这个队列中存在死信消息时 RabbitMQ 就会自动地将这个消息新发布到设置的 DLX(死信交换机) 上去,进而被路由到另一个队列,即死信队列。我们可以监听这个死信交换机的死信队列中的消息、以进行相应的处理。
当消息在一个队列中变成死信之后,如果你对该队列配置了死信队列,那么它能被重新发送到另一个交换机中,这个交换器就是 DLX(死信交换机),而于 DLX 绑定的队列就称之为死信队列。
那种消息会变成死信?
- 消息过期,消息在的存活时间超过所设置的 TTL 时间。
- 消息被拒绝。调用了 channel.basicNack 或 channel.basicReject方法,井且设置 requeue 参数为false。
- 队列的接收消息数长度达到最大长度。
基于消息过期配置死信队列案例:
mq:
queueBinding:
queue: prod_queue_pay
dlQueue: dl-queue
exchange:
name: exchang_prod_pay
dlTopicExchange: dl-topic-exchange
key: prod_pay
dlRoutingKey: dl-routing-key
//==============创建死信交换机并于死信队列进行绑定====================
@Value("${mq.queueBinding.exchange.dlTopicExchange}")
private String dlTopicExchange;
@Value("${mq.queueBinding.dlRoutingKey}")
private String dlRoutingKey;
@Value("${mq.queueBinding.dlQueue}")
private String dlQueue;
//创建死信交换机 【可以是任意类型的交换机,这里采用的是topic类型的】
@Bean
public TopicExchange dlTopicExchange(){
return new TopicExchange(dlTopicExchange,true,false);
}
//创建死信队列
@Bean
public Queue dlQueue(){
return new Queue(dlQueue,true);
}
//死信队列与死信交换机进行绑定
@Bean
public Binding BindingErrorQueueAndExchange(Queue dlQueue, TopicExchange dlTopicExchange){
return BindingBuilder.bind(dlQueue).to(dlTopicExchange).with(dlRoutingKey);
}
//==============创建要执行我们义务的交换机与队列====================
//==============我们要基于队列设置过期时间=========================
@Value("${mq.queueBinding.queue}")
private String queueName;
@Value("${mq.queueBinding.exchange.name}")
private String exchangeName;
@Value("${mq.queueBinding.key}")
private String key;
private final String dle = "x-dead-letter-exchange"; // 必须叫这个名称
private final String dlk = "x-dead-letter-routing-key"; // 必须叫这个名称
private final String ttl = "x-message-ttl";
/**
* 业务队列
* @return
*/
@Bean
public Queue payQueue(){
Map<String,Object> params = new HashMap<>();
//设置队列的过期时间
//队列中所有消息都有相同的过期时间
params.put(ttl,10000);
//==============================================================声明当前队列绑定的死信交换机============================================================================================
params.put(dle,dlTopicExchange);
//声明当前队列的死信路由键 如果没有指定,则使用原队列的路由键。因为我们指定的死信交换机是topic,所以会有路由键。如果是finaot模式,就可不配路由键。
params.put(dlk,dlRoutingKey);
return QueueBuilder.durable(queueName).withArguments(params).build();
}
@Bean
public TopicExchange payTopicExchange(){
return new TopicExchange(exchangeName,true,false);
}
//队列与交换机进行绑定
@Bean
public Binding BindingPayQueueAndPayTopicExchange(Queue payQueue, TopicExchange payTopicExchange){
return BindingBuilder.bind(payQueue).to(payTopicExchange).with(key);
}
// 生产者发送消息
@Component
@Slf4j
public class RabbitSender {
@Value("${mq.queueBinding.exchange.name}")
private String exchangeName;
@Value("${mq.queueBinding.key}")
private String key;
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg){
log.info("RabbitSender.send() msg = {}",msg);
// 将消息发送给业务交换机
rabbitTemplate.convertAndSend(exchangeName,key,msg);
}
}
启动服务,可以看到同时创建了业务队列、业务交换机以及死信队列、死信交换机。而且可以看到业务队列上带了 DLX、DLK标签。
然后调用接口:http://localhost:8080/?msg=红红火火 ,消息会被发送到 prod_queue_pay这
如果 10s 内没有消费者消费这条消息,那么判定这条消息为过期消息。由于设置了 DLX ,过期时消息被丢给 dlxExchange 交换机中,根据所配置的dlRoutingKey 找到与 dlxExchange 匹配的队列 dlQueue后,消息被存储在 dlxQueue这个死信队列中。
15. RabbitMQ 的重试机制
消费端在处理消息过程中可能会报错,此时该如何重新处理消息呢?解决方案有以下几种:
- 开启RabbitMQ的重试机制(默认不开启),让该消费者重试消费消息,有可能能解决(如果是因为突然的网络问题这种有可能重试能解决,如果就是代码逻辑有问题,重试肯定还是发报错)。
- 开启RabbitMQ的重试机制后,使用redis记录重试次数,达到指定次数后,拒绝该消息。
- 在有可能有异常的地方直接try-cathch捕获了,或者把整个方法的逻辑都try-cathch捕获了,捕获异常后,直接拒绝消息(拒绝的话就会变成死信,若配置了死信队列,就会转发到死信队列)。
★★★★★★RabbitMQ中拒绝消息,有2个方法可以调用,他们都有个属性 boolean requeue 。 如果你设置的是true,表示将其放回队列再次发送,如果再次发送还失败,会发生一直不断发送,形成了‘死锁’。★★★★★★
重试并不是RabbitMQ重新发送了消息,仅仅是消费者内部进行的重试,换句话说就是重试跟mq没有任何关系。
原文:https://juejin.cn/post/6979390382371143694#heading-0
# retry功能的开启:
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者进行重试次数限制 默认情况下是 false ====配置yml重试策略=====
max-attempts: 5 # 最大重试次数
max-interval: 10000 # 重试最大间隔时间
initial-interval: 3000 # 重试初始间隔时间
# ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆:重试并不是RabbitMQ重新发送消息给队列,队列再发送给消费者。 而是队列里面的消息重新发送给消费者。
情况一:
消费者在处理消息的过程中会发生异常。
若你开启了重试机制,并指定了重试次数,在重试完次数后,消费者还没成功消费该消息(ack该消息),那么该消息就会处于未消费状态(Unacked)(抛出ListenerExecutionFailedException异常)。
情况二:
消费者在处理消息的过程中会发生异常。
当发生异常时,你使用了try-catch进行了捕获,捕获后做了处理也就是在catch里面,手动ack告诉了rabbit你消费了该消息。
那么这种情况就不会重试,因为你捕获了异常且手动ack了。
情况三:
消费者在处理消息的过程中会发生异常。
当发生异常时,你使用了try-catch进行了捕获,捕获后做了处理也就是在catch里面,手动告诉MQ拒绝消息,并设置requeue为true(让其回到队列立即重新发送)。
requeue:参数设置为 true,则 RabbitMQ 会重新将这条消息存入队列,又再次发送(若有多个消费者,就会发给其他消费者,其他消费者如果逻辑也是一样的【使用了try-catch进行了捕获,捕获后做了处理也就是在catch里面,手动告诉MQ拒绝消息,并设置requeue为true】)
则就会发生死循环。
// 注意:若以最好不要将requeue设置为true,设置成false,就会使该消息变成死信消息,就能搭配死信队列做处理。
16. 如何保证RabbitMQ不重复消费消息
前言:
这玩意并不是都要解决,要看具体情况。
比如:新增订单,你的数据库中本来对于订单编号就有唯一限制,又或者该操作并不是 非幂等性操作,就没必要解决
11.1 为什么要解决重复消费问题
幂等性操作
:就是指比如删除操作,这类操作执行多少次都没影响。
非幂等性操作
:添加操作,而且数据库还是自增的,这类操作执行多次和执行一次差别是很大的!
所以,针对非幂等性操作,一定要保证消息不被重复消费。
11.2 重复消费消息的原因
回想一下消息到消费者的流程: 生产者——————>RabbitMQ——————>消费者。 可以看出,可能出现以下两种情况:
- 生产消息重复
- 消费消息重复
总结:不管是情况1还是情况2,只要保证消费者在消费消息的时候,不重复消费即可,所以我们只需要在消费消息的时候做处理即可。
消费消息时重复消费
消费者消费成功后,再给MQ确认的时候出现了网络波动。MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。
现象示例:https://blog.csdn.net/chenping1993/article/details/114580954
解决思路:
方法一(去重表):添加一张去重表,让每次消费之前,在数据表中添加一条记录,该记录其中某些字段添加唯一属性,若发生重复,则数据添加不上,添加上了才让其进消费者方消费。
方法二(全局唯一ID):让每个消息携带一个全局的唯一ID,引入redis,在消息被消费之前,将消息的唯一ID放到redis中,并设置它的值。
如值为0:正在执行业务,值为1:执行业务成功。
注意:一个比较极端的情况,消费者设置redis值为0后,执行业务,出现死锁,一直执行下去。所以,我们可以为这个redis设置一个过期时间,比如10秒之后,这个redis就消失。
具体消费过程为:
1. 消费者获取到消息后先根据id去查询redis/db是否存在该消息
2. 如果不存在,则设置redis的值为0(执行业务中,并设置过期时间),消费完毕后设置redis值为1(执行业务成功,并设置过期时间),并ack告诉MQ。
3. 如果存在,判断其状态,若为1则证明消息被消费过,直接ack,若为0不执行任何操作。
// 生产者发布消息
@Test
void contextLoads() throws IOException {
// 用于创建消息的唯一标识
CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
// rabbitMQ的convertAndSend() 方法就有一个带消息唯一标识的重载
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId);
System.in.read();
}
@Autowired
private StringRedisTemplate redisTemplate;
// 消费者监听消息
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
//0. 获取MessageId 是从消息头里面的 spring_returned_message_correlation 获得的
String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
//1. 设置key到Redis setIfAbsent:就相当于 setnx【在指定的 key 不存在时,为 key 设置指定的值】
if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {
//2. 消费消息
System.out.println("接收到消息:" + msg);
//3. 设置key的value为1
redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);
//4. 手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else {
//5. 获取Redis中的value即可 如果是1,手动ack
if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
17. 延迟队列
延迟队列:消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
https://blog.csdn.net/weixin_60257072/article/details/128218999
场景举例:
下订单后,30分钟如果还没支付,则取消订单回滚库存。
该场景当然也可以用定时器来解决,每隔几分钟查看对方支付没得。太不优雅,如果数量梁很大的时候不好用
注意:在RabbitMQ中,没有延迟队列的概念,但是我们可以利用ttl和死信队列达到延迟的效果。
实现原理:
- 生产者生产一个消息到队列1
- 队列1中的消息并没有消费者,等它消息过期,被转发到死信队列
- 消费者获取死信队列的信息进行消费
18. RabbitMQ 消息积压问题
消息积压:指的是在消息队列中积压了一定量的消息,但消费者无法及时消费的情况。也就是说消息消费速率小于消息生产速率
。
危害:有可能导致消费端宕机。
如何解决:
上线前:
通过压测,判断预估流量,若预计每秒产生3000条消息,而你的消费者每秒能消费500条消息。那么就需要事先多部署几台消费者。或者优化消费消息的过程。
已上线:
做了什么活动,流量激增。
方法1:扩容消费者,或者设置限流(在MQ的配置中配置"最大消费者数量"与"每次从队列中获取的消息数量")
方法2:给消息设置时间,超时就丢弃,保证不宕机。
方法3:发送者发送流量太大上线更多的消费者,紧急上线专门用于记录消息的队列,将消息先批量取出来,记录数据库,离线慢慢处理。
19. RabbitMQ 消息顺序性
前言:在RabbitMQ中,队列中的消息是有序的,先进先出
。
但是,消费者的消费效率可能是不同的。
举例:
生产者按顺序发送了3条消息给一个队列,该队列有3个消费者,恰好3个消费者依次获取到了3条消息,每个消费者1条消息。
消息A、B、C按顺序进入队列,消费者A1拿到消息A、消费者B1拿到消息B, 结果消费者B执行速度快,就跑完了,又或者消费者A1挂了,都会导致消息顺序不一致!!!
解决方案:
就是一个队列只有一个消费者!!!
即:将要保证顺序性的消息,放到同一个队列,该队列的消息被同一个消费者消费。
消息A、B、C按顺序进入队列,消费者从队列中依次获取消息消费,这样就保证消息顺序一致!!!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用
· DeepSeek火爆全网,官网宕机?本地部署一个随便玩「LLM探索」
· Janus Pro:DeepSeek 开源革新,多模态 AI 的未来
· 上周热点回顾(1.20-1.26)
· 【译】.NET 升级助手现在支持升级到集中式包管理