一、前言
在实际业务中,我们经常遇到需要延迟处理消息的需求:比如用户下单后 30 分钟未支付自动取消订单、用户注册后 30 分钟发送欢迎邮件、物流签收后 3 天自动评价、失败任务延迟5分钟重试等。那么这时候就需要使用"延时任务"方式来实现对应的场景,而延时队列是一种非常实用的消息队列模式,它允许消息在指定时间后才被消费者消费。
传统做法可能是用定时任务轮询数据库,但这种方式存在延迟不精确、数据库压力大、扩展性差等问题。使用延时队列可以将延迟逻辑交给消息中间件处理,实现解耦和高精度定时。
二、RabbitMQ 实现延时队列的两种方式
方案一:基于死信交换机(DLX)+ 消息 TTL(无插件)
这是 RabbitMQ 官方原生支持的方式,无需安装额外插件,兼容性好,但配置相对繁琐,且存在"队头阻塞"问题。
1.核心原理
利用消息的 生存时间(TTL, Time-To-Live) 和 死信交换机(DLX, Dead Letter Exchange) 机制:
1.创建一个普通队列 delay_queue,为其设置 x-dead-letter-exchange(死信交换机)和 x-dead-letter-routing-key(死信路由键),并设置 x-message-ttl(消息存活时间)或通过单条消息的 expiration 属性来控制延迟时间。
2.消息先发送到 delay_queue,等待 TTL 过期后,消息被自动转发到指定的死信交换机,再路由到真正的消费队列 business_queue。
3.消费者监听业务队列,处理延迟后的消息。
优点:无需安装插件,通用性强。
缺点:
1.每个延迟粒度需要一个独立的队列(如果不同消息需要不同延迟时间,需要创建多个队列);消息在队列中堆积时会占用内存,TTL 过期后才被转发。
2.队头阻塞: 如果延时队列中第一条消息设置了 60 秒过期,第二条消息设置了 5 秒过期,预期:第二条消息 5 秒后执行。实际:RabbitMQ 队列是 FIFO 的。第一条消息没过期,第二条消息即使过期了也无法出队。必须等第一条消息过期后,第二条才能被投递到死信交换机。
方案二:延迟插件( rabbitmq-delayed-message-exchange )(推荐方案)
为了解决"队头阻塞"和"多队列管理"的问题,RabbitMQ 官方推出了 rabbitmq_delayed_message_exchange 插件。
1.核心原理
RabbitMQ 官方提供了一个延时消息插件,它实现了一种新的交换机类型 x-delayed-message。发送消息时可以指定 x-delay 头,交换机将根据该头将消息延迟到指定时间后再路由到队列。
1.生产者发送消息时,在 Header 中指定 x-delay(延迟时间)。
2.消息到达交换机后,暂存在交换机内部(而不是队列中),不立即入队。
3.达到延迟时间后,交换机再将消息路由到绑定的队列中。
4.消费者正常消费。
优点:支持任意延迟时间,不存在队头阻塞问题,单一队列可处理多种延迟粒度,实现简单。
缺点:需要安装插件,对 RabbitMQ 版本有要求(3.5.0 以上)。
三、Spring Boot + RabbitMQ如何实现延时消费?
方案一:DLX + TTL 实现延时队列
3.1 添加Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2 配置类
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
@Configuration
public class RabbitConfig {
//文件合并mq
@Value(value = "${file.merge.directExchange:file.merge.directExchange}")
private String fileMergeDirectExchange;
@Value(value = "${file.merge.routingKey:file.merge.routingKey}")
private String fileMergeRoutingKey;
@Value(value = "${file.merge.queueName:file.merge.queueName}")
private String fileMergeQueueName;
//文件合并mq延迟队列
@Value(value = "${file.merge.dlxDirectExchange:file.merge.dlxDirectExchange}")
private String fileMergeDlxDirectExchange;
@Value(value = "${file.merge.dlxRoutingKey:file.merge.dlxRoutingKey}")
private String fileMergeDlxRoutingKey;
@Value(value = "${file.merge.delayQueueName:file.merge.delayQueueName}")
private String fileMergeDelayQueueName;
@Autowired
RabbitAdmin rabbitAdmin;
// 主队列(消息消费失败后进入死信队列)
@Bean
public Queue fileMergeQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", fileMergeDlxDirectExchange); // 死信交换机名
args.put("x-dead-letter-routing-key", fileMergeDlxRoutingKey); // 死信路由键
Queue queue = new Queue(fileMergeQueueName, true, false, false, args);
addQueue(queue);
return queue;
}
// 延时队列(消息在此等待TTL到期后转发到主消费队列)
@Bean
public Queue fileMergeDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", rabbitMQInstanceConfig.getCorpusFileMergeDuration()*1000); // 队列级别TTL(600秒)
args.put("x-dead-letter-exchange", fileMergeDirectExchange); // 过期后转发到主交换机
args.put("x-dead-letter-routing-key",fileMergeRoutingKey);
Queue queue = new Queue(fileMergeDelayQueueName, true, false, false, args);
if (rabbitMQInstanceConfig.getDelayQueueDeleteFlag()) {
Properties properties = rabbitAdmin.getQueueProperties(fileMergeDelayQueueName);
if (Objects.nonNull(properties)) {
Integer QUEUE_MESSAGE_COUNT = (Integer) properties.get(RabbitAdmin.QUEUE_MESSAGE_COUNT);
if (Objects.isNull(QUEUE_MESSAGE_COUNT) || QUEUE_MESSAGE_COUNT<1) {
rabbitUtil.deleteQueue(fileMergeDelayQueueName);
}
}else {
rabbitUtil.deleteQueue(fileMergeDelayQueueName);
}
}
addQueue(queue);
return queue;
}
// 死信交换机(接收主队列的失败消息,路由到延时队列)
@Bean
public DirectExchange fileMergeDlxDirectExchange() {
DirectExchange exchange = new DirectExchange(fileMergeDlxDirectExchange);
rabbitUtil.addExchange(exchange);
return exchange;
}
// 主交换机(接收延时队列的过期消息,路由到主队列)
@Bean
public DirectExchange corpusFileMergeDirectExchange() {
DirectExchange exchange = new DirectExchange(fileMergeDirectExchange);
rabbitUtil.addExchange(exchange);
return exchange;
}
// 绑定:死信交换机 → 延时队列
@Bean
public Binding fileMergeDlxBinding() {
Binding binding = BindingBuilder.bind(fileMergeDelayQueue())
.to(fileMergeDlxDirectExchange()).with(fileMergeDlxRoutingKey);
rabbitAdmin.declareBinding(binding);
return binding;
}
// 绑定:主交换机 → 主队列
@Bean
public Binding fileMergeBinding() {
Binding binding = BindingBuilder.bind(fileMergeQueue())
.to(corpusFileMergeDirectExchange()).with(fileMergeRoutingKey);
rabbitAdmin.declareBinding(binding);
return binding;
}
public void addQueue(Queue queue) {
String queueName = this.rabbitAdmin.declareQueue(queue);
if (StringUtils.isEmpty(queueName)) {
throw new RuntimeException(" add Queue error." + queue.getName());
} else {
logger.info("add Queue OK :{}", queue.getName());
}
}
public boolean deleteQueue(String queueName) {
return this.rabbitAdmin.deleteQueue(queueName);
}
public void addExchange(AbstractExchange exchange) {
this.rabbitAdmin.declareExchange(exchange);
logger.info("已添加 Exchange:{}", exchange.getName());
}
}
3.3 生产者发送消息
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DelayMessageProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitConfig rabbitConfig;
public void sendFileMergeMessage(String message) {
// 发送到默认交换机,路由键为 DELAY_ROUTING_KEY
rabbitTemplate.convertAndSend(rabbitConfig.getFileMergeDirectExchange(),rabbitConfig.getFileMergeRoutingKey(), message);
System.out.println("发送延时消息:" + message);
}
}
3.4 消费者监听业务队列
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class DelayMessageConsumer {
@RabbitListener(queues = "${file.merge.queueName:file.merge.queueName}")
public void handleMessage(String message) {
System.out.println("消费延时消息:" + message);
// 这里执行业务逻辑
}
}
说明:上述代码中,消息先发送到主队列,当消费失败时,消息会到延时队列中,等待 30 秒后自动转发到 主队列 被消费。如果希望不同消息有不同的延迟时间,可以去掉队列的 x-message-ttl,改为在发送消息时设置 expiration 属性(单位毫秒),但需注意消息级 TTL 的过期检查机制可能导致延迟不精确,通常推荐使用多个不同 TTL 的延时队列。
方案二:使用 delayed-message-exchange 插件
1.安装插件
首先需要安装插件,并启用。
# 下载插件(版本对应 RabbitMQ 版本)
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.12.0/rabbitmq_delayed_message_exchange-3.12.0.ez
# 复制到插件目录
cp rabbitmq_delayed_message_exchange-3.12.0.ez /usr/lib/rabbitmq/plugins/
# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 重启 RabbitMQ
systemctl restart rabbitmq-server
2.Java (Spring Boot) 代码实现
配置类
@Configuration
public class DelayConfig {
// 1. 定义自定义的延迟交换机
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
// 核心:指定交换机类型为 x-delayed-message
args.put("x-delayed-type", "direct");
// 参数:交换机名称,类型,是否持久化,是否自动删除,额外参数
return new CustomExchange("delay.plugin.exchange", "x-delayed-message", true, false, args);
}
// 2. 业务队列
@Bean
public Queue businessQueue() {
return QueueBuilder.durable("plugin.business.queue").build();
}
// 3. 绑定
@Bean
public Binding pluginBinding() {
return BindingBuilder.bind(businessQueue())
.to(delayExchange())
.with("plugin.routing.key")
.noargs();
}
}
生产者发送(关键点:设置 Header)
@RestController
@RequestMapping("/task")
public class TaskController {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/send")
public String sendDelayTask() {
String message = "Task_999";
// 设置消息头,单位:毫秒
MessagePostProcessor processor = messageProps -> {
messageProps.getMessageProperties().setHeader("x-delay", 30000); // 延迟 30 秒
return messageProps;
};
rabbitTemplate.convertAndSend(
"delay.plugin.exchange", // 交换机
"plugin.routing.key", // 路由键
message, // 消息体
processor // 处理器(设置延迟时间)
);
return "任务已发送,30秒后执行";
}
}
消费者监听
@Component
public class TaskConsumer {
@RabbitListener(queues = "plugin.business.queue")
public void handleTask(String message) {
System.out.println("执行延时任务: " + message);
}
}
四、消费端的注意事项
1.幂等性:消息可能因为网络重试、重复投递等原因被消费多次,消费者应保证业务幂等。
2.手动确认:生产环境建议使用手动确认(ACK),确保消息被正确处理后再从队列移除,防止消息丢失。
3.异常处理:如果消费过程中出现异常,可以根据需要选择重新入队、转入死信队列或记录错误。
4.监控:关注队列堆积情况,设置合理的 TTL 和队列长度限制,避免消息积压导致内存溢出。
五、总结
1.在分布式系统中实现延时任务,首选 rabbitmq_delayed_message_exchange 插件方案,仅在无法安装插件的受限环境中才考虑原生的 TTL + 死信队列 (DLX) 方案。
2.如果只是简单固定的延迟场景,可以考虑考虑原生的 TTL + 死信队列 (DLX) 方案。
3.若需灵活延迟、易维护,那么选择延时插件。
当然,无论哪种方案,都需关注消息幂等和确认机制,保证最终一致性。根据实际业务选择合适的方式,构建出可靠的延时消息处理系统。