9. RabbitMQ 消息队列幂等性,优先级队列,惰性队列的详细说明
文章目录
- [9. RabbitMQ 消息队列幂等性,优先级队列,惰性队列的详细说明](#9. RabbitMQ 消息队列幂等性,优先级队列,惰性队列的详细说明)
- [1. RabbitMQ 消息队列的 " 幂等性 " 的问题](#1. RabbitMQ 消息队列的 “ 幂等性 ” 的问题)
-
- [1.1 RabbitMQ 消息队列的"幂等性"的概念](#1.1 RabbitMQ 消息队列的“幂等性”的概念)
- [2. RabbitMQ 消息队列的 " 优先级队列 " 的问题](#2. RabbitMQ 消息队列的 “ 优先级队列 ” 的问题)
-
- [2.1 创建交换机](#2.1 创建交换机)
- [2.2 创建队列](#2.2 创建队列)
- [2.3 队列绑定交换机](#2.3 队列绑定交换机)
- [2.4 RabbitMQ 结合 Spring Boot (分模块微服务)实现 "优先级队列"](#2.4 RabbitMQ 结合 Spring Boot (分模块微服务)实现 “优先级队列”)
- [3. RabbitMQ 消息队列的 " 惰性队列 " 的问题](#3. RabbitMQ 消息队列的 “ 惰性队列 ” 的问题)
-
- [3.1 RabbitMQ 消息队列 " 惰性队列 "的概念](#3.1 RabbitMQ 消息队列 “ 惰性队列 ”的概念)
- [3.2 基于策略方式设定](#3.2 基于策略方式设定)
- [3.3 在声明队列时使用参数设定](#3.3 在声明队列时使用参数设定)
- [3.4 实操演练](#3.4 实操演练)
- [4. 最后:](#4. 最后:)
1. RabbitMQ 消息队列的 " 幂等性 " 的问题
1.1 RabbitMQ 消息队列的"幂等性"的概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子:那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入到事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
消息消费时的幂等性(消息不被重复消费)
同一个消息,第一次接收,正常处理业务,如果该消息第二次再接收,那就不能再处理业务,否则就处理重复了;
**幂等性是:**对于一个资源,不管你请求一次还是请求多次,对该资源本身造成的影响应该是相同的,不能因为重复的请求而对该资源重复造成影响;
以接口幂等性举例:
接口幂等性是指: 一个接口用同样的参数反复调用,不会造成业务错误,那么这个接口就是具有幂等性的;
注册接口;
发送短信验证码接口; 比如同一个订单我支付两次,但是只会扣款一次,第二次支付不会扣款,这说明这个支付接口是具有幂等性的;
如何避免消息的重复消费问题?(消息消费时的幂等性)
全局唯一 ID + Redis
生产者在发送消息时,为每条消息设置一个全局唯一的
messageId
,消费者拿到消息后,使用setnx
命令,将messageId
作为key
放到Redis
中:setnx(messageId, 1)
;
- 若返回1,说明之前没有消费过,正常消费;
- 若返回0,说明这条消息之前已消费过,抛弃;
这里是利用 Redis 的一个 setnx 不可重复,原子性的特征。
消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但 实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
解决思路
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消 息时用该 id 先判断该消息是否已消费过。
消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:
- a. 唯一 ID+指纹码机制, 利用数据库主键去重
- b.利用 redis 的原子性去实现
唯一 ID+指纹码机制
指纹码: 我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基 本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存 在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数 据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
Redis 原子性
关于 Redis 的内容,感兴趣的大家可以移步至:✏️✏️✏️ Redis_ChinaRainbowSea的博客-CSDN博客
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
2. RabbitMQ 消息队列的 " 优先级队列 " 的问题
在我们系统中有一个订单催付 的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是 , tmall 商家对我们来说,肯定是要分为大客户 和小客户 的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,它们的订单必须得到优先处理,而曾经我们的后端系统是使用 Redis 来存放的定时轮询,大家都知道 Redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认的优先级。
如何添加:
- 控制台页面添加



- 队列代码中添加优先级
java
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);

- 消息中代码添加优先级
java
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
- 注意事项:
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费,因为:这样才有机会对消息进行排序
简单的来说就是:我们需要让生产者(含有优先级)发送的消息,必须让其存放在队列当中,并且要是到达一定的量,让其在队列当中进行可以有一个时间进行一个(根据优先级上进行一个排序),不然,如果只有一个,就不需要将队列进行排序,因为只有一个就不存在一个优先级的问题。只有存在多个的时候,这样才有机会对消息进行一个排序。
实战:消息队列的"优先级队列" 的具体代码实现:
- 生产者------发送消息
java
package com.rainbowsea.rabbitmq.nine;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
try (Channel channel = RabbitMQUtils.getChannel();) {
//给消息赋予一个 priority 属性
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
for (int i = 1; i < 11; i++) {
String message = "info" + i;
if (i == 5) {
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
} else {
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
}
System.out.println("发送消息完成:" + message);
}
}
}
}
- 消费者------消费
java
package com.rainbowsea.rabbitmq.nine;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import java.util.HashMap;
import java.util.Map;
public class Consumer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
System.out.println("消费者启动等待消费......");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:" + receivedMessage);
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, (consumerTag) -> {
System.out.println("消费者无法消费消息时调用,如队列被删除");
});
}
}
2.1 创建交换机
exchange.test.priority
2.2 创建队列
queue.test.priority
x-max-priority


2.3 队列绑定交换机


2.4 RabbitMQ 结合 Spring Boot (分模块微服务)实现 "优先级队列"
生产者发送消息
1. 配置POM
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2. 配置 YAML 也可以使用 properties
yaml
spring:
rabbitmq:
host: 192.168.200.100
port: 5672
username: guest
password: 123456
virtual-host: /
3. 主启动类
java
package com.rainbowsea.mq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitMQPriorityProducer {
public static void main(String[] args) {
SpringApplication.run(RabbitMQPriorityProducer.class, args);
}
}
4. 发送消息
- 不要启动消费者程序,让多条不同优先级的消息滞留在队列中
- 第一次发送优先级为1的消息
- 第二次发送优先级为2的消息
- 第三次发送优先级为3的消息
- 先发送的消息优先级低,后发送的消息优先级高,将来看看消费端是不是先收到优先级高的消息
第一次发送优先级为1的消息
java
package com.rainbowsea.mq.test;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RabbitMQTest {
public static final String EXCHANGE_PRIORITY = "exchange.test.priority";
public static final String ROUTING_KEY_PRIORITY = "routing.key.test.priority";
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage() {
rabbitTemplate.convertAndSend(EXCHANGE_PRIORITY, ROUTING_KEY_PRIORITY, "I am a message with priority 1.", message->{
message.getMessageProperties().setPriority(1);
return message;
});
}
}
第二次发送优先级为2的消息
java
@Test
public void testSendMessage() {
rabbitTemplate.convertAndSend(EXCHANGE_PRIORITY, ROUTING_KEY_PRIORITY, "I am a message with priority 2.", message->{
message.getMessageProperties().setPriority(2);
return message;
});
}
第三次发送优先级为3的消息
java
@Test
public void testSendMessage() {
rabbitTemplate.convertAndSend(EXCHANGE_PRIORITY, ROUTING_KEY_PRIORITY, "I am a message with priority 3.", message->{
message.getMessageProperties().setPriority(3);
return message;
});
}

消费端接收消息:
- 配置POM
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependencies>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
- 配置YAML
yaml
spring:
rabbitmq:
host: 192.168.200.100
port: 5672
username: guest
password: 123456
virtual-host: /
3. 主启动类
java
package com.rainbowsea.mq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitMQPriorityConsumer {
public static void main(String[] args) {
SpringApplication.run(RabbitMQPriorityConsumer.class, args);
}
}
4.监听器
java
package com.rainbowsea.mq.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyMessageProcessor {
public static final String QUEUE_PRIORITY = "queue.test.priority";
@RabbitListener(queues = {QUEUE_PRIORITY})
public void processPriorityMessage(String data, Message message, Channel channel) throws IOException {
log.info(data);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
5. 测试效果
对于已经滞留服务器的消息,只要消费端一启动,就能够收到消息队列的投递,打印效果如下:
3. RabbitMQ 消息队列的 " 惰性队列 " 的问题
3.1 RabbitMQ 消息队列 " 惰性队列 "的概念
"惰性队列" 的应用场景:
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消 费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持 更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致 使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留 一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的 时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法, 但是效果始终不太理想,尤其是在消息量特别大的时候。
官网说明

队列可以创建为默认
或惰性
模式,模式指定方式是:
- 使用队列策略(建议)
- 设置
queue.declare
参数
如果策略和队列参数同时指定,那么队列参数有更高优先级。如果队列模式是在声明时通过可选参数指定的,那么只能通过删除队列再重新创建来修改。
3.2 基于策略方式设定
shell
# 登录Docker容器
docker exec -it rabbitmq /bin/bash
# 运行rabbitmqctl命令
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
命令解读:
-
rabbitmqctl
命令所在目录是:/opt/rabbitmq/sbin
,该目录已配置到Path环境变量 -
set_policy
是子命令,表示设置策略 -
Lazy
是当前要设置的策略名称,是我们自己自定义的,不是系统定义的 -
"^lazy-queue$"
是用正则表达式限定的队列名称,凡是名称符合这个正则表达式的队列都会应用这里的设置 -
'{"queue-mode":"lazy"}'
是一个JSON格式的参数设置指定了队列的模式为"lazy" -
---apply-to参数指定该策略将应用于队列(queues)级别
-
命令执行后,所有名称符合正则表达式的队列都会应用指定策略,包括未来新创建的队列
如果需要修改队列模式可以执行如下命令(不必删除队列再重建):
shell
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"default"}' --apply-to queues
3.3 在声明队列时使用参数设定
- 参数名称:x-queue-mode
- 可用参数值:
- default
- lazy
- 不设置就是取值为default
Java代码原生API设置方式:
java
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
Java代码注解设置方式:
java
@Queue(value = QUEUE_NAME, durable = "true", autoDelete = "false", arguments = {
@Argument(name = "x-queue-mode", value = "lazy")
})
队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy 模式即为惰性队列的模式,可以通过调用
channel.queueDeclare
方法的时候在参数中设置,也可以通过 Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。在队列声明的时候可以通过
"x-queue-mode"
参数来设置队列的模式,取值为"default"
和"lazy"
。下面示 例中演示了一个惰性队列的声明细节:
javaMap<String, Object> args = new HashMap<String, Object>(); args.put("x-queue-mode", "lazy"); channel.queueDeclare("myqueue", false, false, false, args);
内存开销对比:

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB
3.4 实操演练
生产者端代码
配置POM
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
配置YAML
yaml
spring:
rabbitmq:
host: 192.168.200.100
port: 5672
username: guest
password: 123456
virtual-host: /
主启动类
java
package com.rainbowsea.mq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitMQLazyProducer {
public static void main(String[] args) {
SpringApplication.run(RabbitMQLazyProducer.class, args);
}
}
发送消息
java
package com.rainbowsea.mq.test;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RabbitMQTest {
public static final String EXCHANGE_LAZY_NAME = "exchange.rainbowsea.lazy";
public static final String ROUTING_LAZY_KEY = "routing.key.rainbowsea.lazy";
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage() {
rabbitTemplate.convertAndSend(EXCHANGE_LAZY_NAME, ROUTING_LAZY_KEY, "I am a message for test lazy queue.");
}
}
消费者端代码
配置POM
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependencies>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
配置YAML
yaml
spring:
rabbitmq:
host: 192.168.200.100
port: 5672
username: guest
password: 123456
virtual-host: /
主启动类
java
package com.rainbowsea.mq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitMQLazyConsumerMainType {
public static void main(String[] args) {
SpringApplication.run(RabbitMQLazyConsumerMainType.class, args);
}
}
监听器
java
package com.rainbowsea.mq.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MyLazyMessageProcessor {
public static final String EXCHANGE_LAZY_NAME = "exchange.rainbowsea.lazy";
public static final String ROUTING_LAZY_KEY = "routing.key.rainbowsea.lazy";
public static final String QUEUE_LAZY_NAME = "queue.rainbowsea.lazy";
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = QUEUE_LAZY_NAME, durable = "true", autoDelete = "false", arguments = {
@Argument(name = "x-queue-mode", value = "lazy")
}),
exchange = @Exchange(value = EXCHANGE_LAZY_NAME, durable = "true", autoDelete = "false"),
key = {ROUTING_LAZY_KEY}
))
public void processMessageLazy(String data, Message message, Channel channel) {
log.info("消费端接收到消息:" + data);
}
}
测试:
-
先启动消费端
-
基于消费端@RabbitListener注解中的配置,自动创建了队列

- 发送消息
4. 最后:
"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"