文章目录
- [一. TTL机制(Time to Live, 过期时间)](#一. TTL机制(Time to Live, 过期时间))
-
- [1. 给队列设置过期时间](#1. 给队列设置过期时间)
- [2. 给信息设置过期时间](#2. 给信息设置过期时间)
- [3. 队列TTL与消息TTL区别](#3. 队列TTL与消息TTL区别)
- [二. 死信队列](#二. 死信队列)
-
- [1. 产生死信的情况](#1. 产生死信的情况)
- [2. 死信的写法](#2. 死信的写法)
- [3. 死信应用场景](#3. 死信应用场景)
- [三. 延迟队列](#三. 延迟队列)
-
- [1. TTL + 死信队列实现延迟队列](#1. TTL + 死信队列实现延迟队列)
- [2. 存在的问题](#2. 存在的问题)
- [3. 下载和配置延迟插件](#3. 下载和配置延迟插件)
- [4. 延迟插件的使用](#4. 延迟插件的使用)
一. TTL机制(Time to Live, 过期时间)
我们可以给队列和消息设置过期时间, 当给队列(Queue)设置了过期时间后, 队列中消息的过期时间是一致的, 当给消息(Message)设置了过期时间后, 该消息会在指定时间后过期, 这里要注意, 当同时给队列和消息设置了过期时间时, 会选取较小的时间
1. 给队列设置过期时间
java
// ttl
@Bean("ttlQueue")
public Queue ttlQueue() {
return QueueBuilder.durable(Constants.TTL_QUEUE)
.ttl(100000)
.build();
}
@Bean("ttlExchange")
public DirectExchange ttlExchange() {
return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();
}
@Bean("ttlBinding")
public Binding ttlBinding(@Qualifier("ttlQueue") Queue queue, @Qualifier("ttlExchange") DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("ttl");
}
2. 给信息设置过期时间
方式一, 使用MessagePostProcessor
java
@RequestMapping("/ttl")
public String ttl() {
MessagePostProcessor messagePostProcessor = message -> {
message.getMessageProperties().setExpiration("5000");
return message;
};
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "Consumer retry mode ...", messagePostProcessor);
return "发送成功";
}
方式二, 使用Message
java
@RequestMapping("/ttl")
public String ttl() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("5000");
Message message = new Message("Consumer ttl mode ...".getBytes(StandardCharsets.UTF_8), messageProperties);
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", message);
return "发送成功";
}
3. 队列TTL与消息TTL区别
队列设置了TTL后, 一旦过了指定时间, 消息过期, 会立即对消息删除, 这是因为给队列设置了TTL后, 已过期的消息肯定在队列首部, 因此只需从队头遍历消息是否过期即可
消息设置了TTL后, 当其过期时, 不会立马从队列中删除, 而是在发送给消费者之前进行判断, 这是因为在一个队列中, 可能每个消息的过期时间都是不同的, 要删除一条消息就需要遍历整个队列, 会比较占用资源, 因此, 如果在同一队列中, 先入队了过期时间较长的消息1, 后入队过期时间短的消息2, 那么当消息2过期时并不会直接删除, 会等待消息1过期删除或者被消费后, 在发送给消费者事前判定时再删除
二. 死信队列
因为一些原因, 导致无法被消费的消息, 就是死信(DL), 而存储死信的队列(DLQ), 就是死信队列, 负责路由死信的交换机, 就是死信交换机(DLX)
1. 产生死信的情况
1. 消息确认机制为手动确认manual时, 消费者调用Back, 消息被拒绝后, 且requeue参数为false, 消息不能重新加入队列, 就产生了死信
2. 消息已过期
3. 队列已满, 又有新消息想要入队时
2. 死信的写法
依照下面图片来写代码, 能看出死信交换机(DLX)与正常队列绑定的, 当队列中的消息出现异常时, 会通过死信交换机路由到死信队列(DLQ)
声明正常队列与交换机, 正常队列与正常交换机绑定后, 再与死信交换机绑定
java
@Bean("normalExchange")
public DirectExchange normalExchange() {
return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();
}
@Bean("normalQueue")
public Queue normalQueue() {
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DL_EXCHANGE) // 再绑定死信交换机
.deadLetterRoutingKey("dlx") // 死信交换机路由的RoutingKey
.build();
}
@Bean("normalBinding")
public Binding normalBinding(@Qualifier("normalQueue") Queue queue, @Qualifier("normalExchange") DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("normal");
}
声明死信队列与死信交换机
java
@Bean("DLExchange")
public DirectExchange DLExchange() {
return ExchangeBuilder.directExchange(Constants.DL_EXCHANGE).build();
}
@Bean("DLQueue")
public Queue DLQueue() {
return QueueBuilder.durable(Constants.DL_QUEUE).build();
}
@Bean("DLBinding")
public Binding DLBinding(@Qualifier("DLQueue") Queue queue, @Qualifier("DLExchange") DirectExchange directExchange) {
return BindingBuilder.bind(queue).to(directExchange).with("dlx");
}
生产者发送消息, 测试死信时, 只需要给交换机发送消息即可, 设置过期时间, 当消息过期后, 会自己根据RoutingKey路由到死信队列
java
@RequestMapping("/dl")
public String dl() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("5000");
Message message = new Message("Consumer dl mode ttl -> 5000ms ...".getBytes(StandardCharsets.UTF_8), messageProperties);
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", message);
return "发送成功";
}
normal.queue 的DLX标志为当定了死信交换机, DLK说明设置了死信交换机的RoutingKey
运行图
5s前
5s后
3. 死信应用场景
1. 支付时, 我们下单后会收到一个30分钟内待付款的消息, 这时如果用户到了过期时间也没有进行支付的话, 就会将消息加入死信队列, 启动一个新消费者来将该订单改为取消状态
2. 支付异常时, 将信息保存下来, 用于人工确认
3. Spring-AMQP在消费者处理消息发生异常时, 可能会不断将消息重新入队来重新发送, 这会让消费者不断地生产消息, 导致消息积压, 这时需要将requeue设置为false, 将消费异常的消息加入死信队列, 启动一个专门的消费者来处理死信
三. 延迟队列
延迟队列即生产者发送消息之后, 不想让消费者立刻就能拿到消息, 而是想等待特定时间之后, 消费者才能获取
对于延迟队列来说, 应用场景是很多的, 例如👇
开空调指定时间关闭
微波炉加热时长
预约会议, 会议开始前10分钟发送通知
注册福利通知, 当用户注册一周/一月/一年时, 发送问卷调查, 发送感谢信等等
1. TTL + 死信队列实现延迟队列
在RabbitMQ中, 并没有直接提供延迟队列功能, 但我们也可以通过设置过期时间+死信队列来间实现延迟队列的功能
这里只简单修改了声明队列, 生产者和消费者的代码, 因此只提供这几部分
java
@Bean("normalQueue")
public Queue normalQueue() {
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DL_EXCHANGE) // 再绑定死信交换机
.deadLetterRoutingKey("dlx") // 死信交换机路由的RoutingKey
.ttl(5000) // 设置队列过期时间为5s
.build();
}
java
@RequestMapping("/delay")
public String delay() {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "Consumer delay Queue.ttl -> 5000ms ...");
System.out.println("生产者发送消息时间: " + new Date().toLocaleString());
return "发送成功";
}
java
@Component
public class DLListeners {
@RabbitListener(queues = Constants.DL_QUEUE)
public void handlerRetry(Message message) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("时间: " + new Date().toLocaleString() + ", 队列[" + Constants.DL_QUEUE + "]接收到消息: " + new String(message.getBody()) + ", deliveryTag: " + deliveryTag);
}
}
2. 存在的问题
已知设置消息过期的方式有两种, 一种是上面给队列设置TTL, 还有一种是给消息单独设置TTL, 这里就出现一个问题, 就是当先入队了TTL较长的消息, 后入队TTL短的消息, 因为这里消息过期不会立即删除的特性, 就会导致TTL短的消息过期后, 不能立即加入死信队列, 因此当TTL较长的消息过期时, TTL较短的消息也必定过期, 最终会和TTL长的消息一起同时丢入死信队列中, 被消费者同时获取
java
@RequestMapping("/delay2")
public String delay2() {
MessageProperties messageProperties1 = new MessageProperties();
messageProperties1.setExpiration("10000");
Message message1 = new Message("Consumer dl message1.ttl -> 10000ms ...".getBytes(StandardCharsets.UTF_8), messageProperties1);
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", message1);
MessageProperties messageProperties2 = new MessageProperties();
messageProperties2.setExpiration("5000");
Message message2 = new Message("Consumer dl message2.ttl -> 5000ms ...".getBytes(StandardCharsets.UTF_8), messageProperties2);
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", message2);
System.out.println("生产者发送消息时间: " + new Date().toLocaleString());
return "发送成功";
}
java
@Bean("normalQueue")
public Queue normalQueue() {
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DL_EXCHANGE) // 再绑定死信交换机
.deadLetterRoutingKey("dlx") // 死信交换机路由的RoutingKey
.build();
}
3. 下载和配置延迟插件
官方为了解决这个问题, 提供了相关插件, 来添加一个新的交换机类型👇
1.RabbitMQ延迟队列插件官方下载
2. 进入release发布界面, 找到自己对应版本的RabbitMQ延迟队列插件👇
3. 最后根据官方文档, 将插件放入对应的plugins目录下👇
因为我这里是使用的docker启动的容器, 且RabbitMQ版本为3.8.30, 下面简单介绍下如何将插件添加进容器, 注意我的容器名为 'oj-rabbit-dev' , 你们替换为自己的容器名
1. 先将插件复制到容器内部目录, 命令格式为👇
docker cp 宿主机文件 容器名称或ID:容器目录
java
以我的举例:
docker cp D:\Users\ran\Downloads\rabbitmq_delayed_message_exchange-3.8.0.ez oj-rabbit-dev:/opt/rabbitmq/plugins/
2. 查看当前插件状态👇
java
docker exec oj-rabbit-dev rabbitmq-plugins list
3. 启动延迟队列插件👇
java
docker exec oj-rabbit-dev rabbitmq-plugins enable rabbitmq_delayed_message_exchange
4. 重启服务👇
java
docker restart oj-rabbit-dev
5. 查看自己是否能添加延迟插件的交换机👇
4. 延迟插件的使用
1. 声明交换机与队列, 及其绑定关系👇
java
package com.ran.extension.config;
import com.ran.extension.constant.Constants;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Created with IntelliJ IDEA.
* Description:
* User: ran
* Date: 2026-04-02
* Time: 20:26
*/
@Configuration
public class DelayConfig {
@Bean("DelayExchange")
public Exchange DelayExchange() {
return ExchangeBuilder.directExchange(Constants.DELAY_EXCHANGE).delayed().build(); // delayed() 方法表示声明的是延迟类型交换机
}
@Bean("DelayQueue")
public Queue DelayQueue() {
return QueueBuilder.durable(Constants.DELAY_QUEUE).build();
}
@Bean("DelayBinding")
public Binding DelayBinding(@Qualifier("DelayQueue") Queue queue, @Qualifier("DelayExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("delay").noargs(); // noargs()方法表示,交换机没有其他参数
}
}
2. 生产者👇
java
@RequestMapping("/delay2")
public String delay2() {
rabbitTemplate.convertAndSend(Constants.DELAY_EXCHANGE, "delay", "delay message -> 10s ...", message -> {
message.getMessageProperties().setDelayLong(10000L);
return message;
});
rabbitTemplate.convertAndSend(Constants.DELAY_EXCHANGE, "delay", "delay message -> 5s ...",message -> {
message.getMessageProperties().setDelayLong(5000L);
return message;
});
System.out.println("生产者发送消息时间: " + new Date().toLocaleString());
return "发送成功";
}
3. 消费者👇
java
@Component
public class DelayListeners {
@RabbitListener(queues = Constants.DELAY_QUEUE)
public void handlerRetry(Message message) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("时间: " + new Date().toLocaleString() + ", 队列[" + Constants.DELAY_QUEUE + "]接收到消息: " + new String(message.getBody()) + ", deliveryTag: " + deliveryTag);
}
}
4. 运行图
5. 延迟交换机, DM表示正在交换机上等待延迟的消息, args表示在延迟结束后, 其内部的路由规则
延迟插件原理是, 有TTL的消息先在交换机上, 等待延迟时间后, 再将消息路由到队列上, 这样即使先发送的消息TTL较长, 后发送的TTL较短, 在交换机上仍是TTL较短的先到期, 然后先路由到队列上













