RabbitMQ 延迟队列
原文(部分调整):【RabbitMQ】一文带你搞定 RabbitMQ 延迟队列
一、说明
上一篇文章详细探讨了 RabbitMQ 中的死信队列,包括其概念、使用场景及实现方法。若读者尚未了解死信队列,建议先行阅读该文章,因为本文内容与死信队列紧密相关。
本文将继续深入探讨 RabbitMQ 的高级特性,通过学习,读者将了解到:
- 什么是延时队列
- 延时队列使用场景
- RabbitMQ 中的 TTL
- 如何利用 RabbitMQ 来实现延时队列
二、本文大纲
本文大纲如下:
![]() |
在阅读本文前,建议读者对 RabbitMQ 及其死信队列有基本了解。
三、什么是延时队列
延时队列首先是一种队列。队列的特点是其内部元素有序,且元素的出队和入队具有明确的方向性,即元素从一端进入,从另一端取出。
其次,延时队列最显著的特性体现在其延时属性上。与普通队列不同,普通队列中的元素通常期望被尽快取出并处理,而延时队列中的元素则期望在指定时间点被取出和处理。因此,延时队列中的元素都带有时间属性,通常代表需要在未来某个时间点执行的消息或任务。
简而言之,延时队列是用于存放需要在指定时间点被处理的元素的队列。
四、延时队列使用场景
那么,延时队列适用于哪些场景呢?可以考虑以下情况:
- 订单在 10 分钟内未支付则自动取消。
- 新创建的店铺,如果在 10 天内未上传商品,则自动发送消息提醒。
- 账单在 1 周内未支付,则自动结算。
- 用户注册成功后,如果 3 天内未登录则进行短信提醒。
- 用户发起退款,如果 3 天内未得到处理则通知相关运营人员。
- 预定会议后,需要在预定时间点前 10 分钟通知各个与会人员参加会议。
这些场景的共同特点是,需要在某个事件发生后或之前的指定时间点完成某项任务。例如:订单生成后,在 10 分钟后检查其支付状态,若未支付则自动关闭;店铺创建后,10 天后检查商品上新数量,并通知上新数为 0 的商户;账单生成后,检查支付状态并自动结算未支付的账单;新用户注册后,3 天后检查其活动数据,并通知没有任何活动记录的用户;发起退款后,3 天后检查订单是否已被处理,若未处理则发送消息给相关运营人员;预定会议后,若距离会议开始仅剩 10 分钟,则通知所有与会人员。
乍看起来,似乎可以使用定时任务,持续轮询数据,每秒查询一次,取出并处理需要的数据即可。如果数据量较少,这种方法确实可行。例如,对于“如果账单 1 周内未支付则进行自动结算”的需求,如果对时间精度要求不高,而是在宽松意义上的 1 周内完成,那么每天晚上运行定时任务检查所有未支付账单,是一个可行的方案。然而,对于数据量庞大且时效性强的场景,如“订单 10 分钟内未支付则关闭”,短时间内未支付的订单数据可能非常多,活动期间甚至可能达到百万或千万级别。对如此庞大的数据量仍采用轮询方式显然不可取,很可能在 1 秒内无法完成所有订单的检查,同时会给数据库带来巨大压力,导致无法满足业务要求且性能低下。
更重要的是,这种方案不够优雅!
追求卓越的开发者,应始终致力于设计更优雅的架构和编写更优美的代码。
此时,延时队列便能发挥其优势,以上场景正是其理想应用之地。
既然延时队列能有效解决特定场景下带时间属性的任务需求,那么如何构建一个延时队列呢?接下来,本文将介绍如何利用 RabbitMQ 来实现延时队列。
五、RabbitMQ 中的 TTL
在介绍延时队列之前,首先需要了解 RabbitMQ 中的一个高级特性——TTL(Time To Live)。
TTL是 RabbitMQ 中消息或队列的一个属性,它表示一条消息或该队列中所有消息的最大存活时间,单位为毫秒。换言之,如果一条消息设置了 TTL 属性或进入了设置 TTL 属性的队列,那么该消息若在 TTL 设定的时间内未被消费,则会成为“死信”(关于“死信”的详细内容,请参考上一篇文章)。如果同时配置了队列的 TTL 和消息的 TTL,则以二者中较小的值为准。
那么,如何设置这个 TTL 值呢?有两种方式,第一种是在创建队列的时候设置队列的x-message-ttl属性,如下:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 6000);
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
这样所有被投递到该队列的消息都最多不会存活超过 6 秒。
另一种方式便是针对每条消息设置TTL,代码如下:
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.expiration("6000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routingKey, mandatory, properties, "msg body".getBytes());
这样这条消息的过期时间也被设置成了 6 秒。
这两种方式存在区别:如果设置了队列的 TTL 属性,一旦消息过期,就会立即被队列丢弃;而第二种方式,即使消息过期,也不一定会被立即丢弃,因为消息是否过期是在即将投递给消费者之前判定的。如果当前队列存在严重的消息积压,那么已过期的消息可能还会存活较长时间。
此外,需要注意的是,若不设置 TTL,消息将永远不会过期;若将 TTL 设置为 0,则表示除非该消息此时能够直接投递给消费者,否则它将被立即丢弃。
六、如何利用 RabbitMQ 实现延时队列
上一篇文章介绍了如何设置死信队列,本文前述又阐述了 TTL。至此,利用 RabbitMQ 实现延时队列的两个核心要素已经具备。接下来,只需将它们结合,即可构建一个延时队列。
思考一下,延时队列的核心需求是让消息延迟指定时间后被处理。TTL 恰好能够使消息在延迟指定时间后成为死信。另一方面,成为死信的消息会被投递到死信队列中,消费者只需持续消费死信队列中的消息即可,因为这些消息都是期望被立即处理的。
从下图可以大致看出消息的流向:
![]() |
生产者生成一条延时消息,根据所需的延迟时间,利用不同的 routingKey 将消息路由到不同的延时队列。每个延时队列都设置了不同的 TTL 属性,并绑定到同一个死信交换机中。消息过期后,会根据死信 routingKey 的不同,被路由到不同的死信队列中,消费者只需监听对应的死信队列并进行处理即可。
下面来看具体代码实现:
先声明交换机、队列以及它们的绑定关系:
@Configuration
public class RabbitMQConfig {
public static final String DELAY_EXCHANGE_NAME = "delay.queue.demo.business.exchange";
public static final String DELAY_QUEUEA_NAME = "delay.queue.demo.business.queuea";
public static final String DELAY_QUEUEB_NAME = "delay.queue.demo.business.queueb";
public static final String DELAY_QUEUEA_ROUTING_KEY = "delay.queue.demo.business.queuea.routingkey";
public static final String DELAY_QUEUEB_ROUTING_KEY = "delay.queue.demo.business.queueb.routingkey";
public static final String DEAD_LETTER_EXCHANGE = "delay.queue.demo.deadletter.exchange";
public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "delay.queue.demo.deadletter.delay_10s.routingkey";
public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "delay.queue.demo.deadletter.delay_60s.routingkey";
public static final String DEAD_LETTER_QUEUEA_NAME = "delay.queue.demo.deadletter.queuea";
public static final String DEAD_LETTER_QUEUEB_NAME = "delay.queue.demo.deadletter.queueb";
// 声明延时 Exchange
@Bean("delayExchange")
public DirectExchange delayExchange(){
return new DirectExchange(DELAY_EXCHANGE_NAME);
}
// 声明死信 Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
// 声明延时队列 A 延时 10s
// 并绑定到对应的死信交换机
@Bean("delayQueueA")
public Queue delayQueueA(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
// x-message-ttl 声明队列的 TTL
args.put("x-message-ttl", 6000);
return QueueBuilder.durable(DELAY_QUEUEA_NAME).withArguments(args).build();
}
// 声明延时队列 B 延时 60s
// 并绑定到对应的死信交换机
@Bean("delayQueueB")
public Queue delayQueueB(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);
// x-message-ttl 声明队列的 TTL
args.put("x-message-ttl", 60000);
return QueueBuilder.durable(DELAY_QUEUEB_NAME).withArguments(args).build();
}
// 声明死信队列 A 用于接收延时 10s 处理的消息
@Bean("deadLetterQueueA")
public Queue deadLetterQueueA(){
return new Queue(DEAD_LETTER_QUEUEA_NAME);
}
// 声明死信队列 B 用于接收延时 60s 处理的消息
@Bean("deadLetterQueueB")
public Queue deadLetterQueueB(){
return new Queue(DEAD_LETTER_QUEUEB_NAME);
}
// 声明延时队列 A 绑定关系
@Bean
public Binding delayBindingA(@Qualifier("delayQueueA") Queue queue,
@Qualifier("delayExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEA_ROUTING_KEY);
}
// 声明业务队列 B 绑定关系
@Bean
public Binding delayBindingB(@Qualifier("delayQueueB") Queue queue,
@Qualifier("delayExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEB_ROUTING_KEY);
}
// 声明死信队列 A 绑定关系
@Bean
public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
}
// 声明死信队列 B 绑定关系
@Bean
public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);
}
}
接下来,创建两个消费者,分别消费两个死信队列中的消息:
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = DEAD_LETTER_QUEUEA_NAME)
public void receiveA(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},死信队列 A 收到消息:{}", new Date().toString(), msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
@RabbitListener(queues = DEAD_LETTER_QUEUEB_NAME)
public void receiveB(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},死信队列 B 收到消息:{}", new Date().toString(), msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
消息生产者代码如下:
@Component
public class DelayMessageSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String msg, DelayTypeEnum type){
switch (type){
case DELAY_10s:
rabbitTemplate.convertAndSend(DELAY_EXCHANGE_NAME, DELAY_QUEUEA_ROUTING_KEY, msg);
break;
case DELAY_60s:
rabbitTemplate.convertAndSend(DELAY_EXCHANGE_NAME, DELAY_QUEUEB_ROUTING_KEY, msg);
break;
}
}
}
接下来,我们通过一个 Web 接口来生产消息:
@Slf4j
@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController {
@Autowired
private DelayMessageSender sender;
@RequestMapping("sendmsg")
public void sendMsg(String msg, Integer delayType){
log.info("当前时间:{},收到请求,msg:{},delayType:{}", new Date(), msg, delayType);
sender.sendMsg(msg, Objects.requireNonNull(DelayTypeEnum.getDelayTypeEnumByValue(delayType)));
}
}
准备就绪,启动应用程序!
打开 RabbitMQ 的管理后台,可以看到我们刚才创建的交换机和队列信息:
![]() |
![]() |
![]() |
接下来,我们发送几条消息,例如访问以下 URL:http://localhost:8080/rabbitmq/sendmsg?msg=testMsg1&delayType=1 和 http://localhost:8080/rabbitmq/sendmsg?msg=testMsg2&delayType=2。
日志如下:
2019-07-28 16:02:19.813 INFO 3860 --- [nio-8080-exec-9] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:02:19 CST 2019,收到请求,msg:testMsg1,delayType:1
2019-07-28 16:02:19.815 INFO 3860 --- [nio-8080-exec-9] .l.DirectReplyToMessageListenerContainer : SimpleConsumer [queue=amq.rabbitmq.reply-to, consumerTag=amq.ctag-o-qPpkWIkRm73DIrOIVhig identity=766339] started
2019-07-28 16:02:25.829 INFO 3860 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:02:25 CST 2019,死信队列A收到消息:testMsg1
2019-07-28 16:02:41.326 INFO 3860 --- [nio-8080-exec-1] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:02:41 CST 2019,收到请求,msg:testMsg2,delayType:2
2019-07-28 16:03:41.329 INFO 3860 --- [ntContainer#0-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:03:41 CST 2019,死信队列B收到消息:testMsg2
第一条消息在 6 秒后变为死信消息并被消费者消费;第二条消息在 60 秒后变为死信消息并被消费。至此,一个基本的延时队列已实现。
然而,这种实现方式存在局限性:每当出现新的延迟时间需求时,就必须新增一个队列。目前仅有 6 秒和 60 秒两个时间选项。如果需要延迟 1 小时处理,则需要新增一个 TTL 为 1 小时的队列。对于预定会议室并提前通知等场景,这种方案将导致需要创建大量队列以满足需求。
因此,深入思考后会发现,该方案并非完美。
七、RabbitMQ 延时队列优化
显然,需要一种更通用的方案来满足需求,这便意味着需要将 TTL 设置在消息属性中。下面我们将进行尝试。
增加一个延时队列,用于接收设置了任意延迟时长的消息,并增加一个相应的死信队列和 routingKey:
@Configuration
public class RabbitMQConfig {
public static final String DELAY_EXCHANGE_NAME = "delay.queue.demo.business.exchange";
public static final String DELAY_QUEUEC_NAME = "delay.queue.demo.business.queuec";
public static final String DELAY_QUEUEC_ROUTING_KEY = "delay.queue.demo.business.queuec.routingkey";
public static final String DEAD_LETTER_EXCHANGE = "delay.queue.demo.deadletter.exchange";
public static final String DEAD_LETTER_QUEUEC_ROUTING_KEY = "delay.queue.demo.deadletter.delay_anytime.routingkey";
public static final String DEAD_LETTER_QUEUEC_NAME = "delay.queue.demo.deadletter.queuec";
// 声明延时 Exchange
@Bean("delayExchange")
public DirectExchange delayExchange(){
return new DirectExchange(DELAY_EXCHANGE_NAME);
}
// 声明死信 Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
// 声明延时队列 C 不设置 TTL
// 并绑定到对应的死信交换机
@Bean("delayQueueC")
public Queue delayQueueC(){
Map<String, Object> args = new HashMap<>(3);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEC_ROUTING_KEY);
return QueueBuilder.durable(DELAY_QUEUEC_NAME).withArguments(args).build();
}
// 声明死信队列 C 用于接收延时任意时长处理的消息
@Bean("deadLetterQueueC")
public Queue deadLetterQueueC(){
return new Queue(DEAD_LETTER_QUEUEC_NAME);
}
// 声明延时列 C 绑定关系
@Bean
public Binding delayBindingC(@Qualifier("delayQueueC") Queue queue,
@Qualifier("delayExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEC_ROUTING_KEY);
}
// 声明死信队列 C 绑定关系
@Bean
public Binding deadLetterBindingC(@Qualifier("deadLetterQueueC") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEC_ROUTING_KEY);
}
}
增加死信队列 C 的消费者:
@RabbitListener(queues = DEAD_LETTER_QUEUEC_NAME)
public void receiveC(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},死信队列 C 收到消息:{}", new Date().toString(), msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
再次启动应用程序!然后访问http://localhost:8080/rabbitmq/delayMsg?msg=testMsg1&delayTime=5000来生产消息,请注意这里的单位是毫秒。
2019-07-28 16:45:07.033 INFO 31468 --- [nio-8080-exec-4] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:45:07 CST 2019,收到请求,msg:testMsg1,delayTime:5000
2019-07-28 16:45:11.694 INFO 31468 --- [nio-8080-exec-5] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:45:11 CST 2019,收到请求,msg:testMsg2,delayTime:5000
2019-07-28 16:45:12.048 INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:45:12 CST 2019,死信队列 C 收到消息:testMsg1
2019-07-28 16:45:16.709 INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:45:16 CST 2019,死信队列 C 收到消息:testMsg2
从结果看似乎没有问题,但需注意,正如前文所述,若 TTL 设置在消息属性上,消息可能不会按时“过期”。这是因为 RabbitMQ 仅会检查队列中的第一个消息是否过期,如果过期则将其投递到死信队列。因此,如果队列中第一个消息的延迟时长很长,而后续消息的延迟时长很短,那么即使后续消息的 TTL 已到期,它们也必须等待第一个消息过期并被处理后才能成为死信,进而被消费者处理。
进行如下实验进行验证:
2019-07-28 16:49:02.957 INFO 31468 --- [nio-8080-exec-8] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:49:02 CST 2019,收到请求,msg:longDelayedMsg,delayTime:20000
2019-07-28 16:49:10.671 INFO 31468 --- [nio-8080-exec-9] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 16:49:10 CST 2019,收到请求,msg:shortDelayedMsg,delayTime:2000
2019-07-28 16:49:22.969 INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:49:22 CST 2019,死信队列 C 收到消息:longDelayedMsg
2019-07-28 16:49:22.970 INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 16:49:22 CST 2019,死信队列 C 收到消息:shortDelayedMsg
我们首先发送了一个延迟时长为 20 秒的消息,随后发送了一个延迟时长为 2 秒的消息。实验结果表明,第二个消息必须等待第一个消息成为死信后才会被处理,即使其自身 TTL 已过期。
八、利用 RabbitMQ 插件实现延迟队列
上文提到的问题确实是一个关键缺陷。若无法在消息粒度上精确设置 TTL 并确保消息在设定时间点及时过期,便无法设计出一个通用的延时队列。
如何解决这一问题呢?可以通过安装 RabbitMQ 插件实现:访问 community-plugins.html 下载rabbitmq_delayed_message_exchange插件,然后将其解压并放置到 RabbitMQ 的插件目录。
接下来,进入 RabbitMQ 安装目录下的 sbin 目录,执行以下命令使该插件生效,并重启 RabbitMQ 服务。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
然后,声明以下 Beans:
@Configuration
public class DelayedRabbitMQConfig {
public static final String DELAYED_QUEUE_NAME = "delay.queue.demo.delay.queue";
public static final String DELAYED_EXCHANGE_NAME = "delay.queue.demo.delay.exchange";
public static final String DELAYED_ROUTING_KEY = "delay.queue.demo.delay.routingkey";
@Bean
public Queue immediateQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
@Bean
public CustomExchange customExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
@Bean
public Binding bindingNotify(@Qualifier("immediateQueue") Queue queue,
@Qualifier("customExchange") CustomExchange customExchange) {
return BindingBuilder.bind(queue).to(customExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
Controller 层再添加一个入口:
@RequestMapping("delayMsg2")
public void delayMsg2(String msg, Integer delayTime) {
log.info("当前时间:{},收到请求,msg:{},delayTime:{}", new Date(), msg, delayTime);
sender.sendDelayMsg(msg, delayTime);
}
消息生产者的代码也需修改为:
public void sendDelayMsg(String msg, Integer delayTime) {
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, msg, a ->{
a.getMessageProperties().setDelay(delayTime);
return a;
});
}
最后,创建一个新的消费者:
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},延时队列收到消息:{}", new Date().toString(), msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
一切准备就绪,启动应用程序!然后分别访问以下 URL:
http://localhost:8080/rabbitmq/delayMsg2?msg=msg1&delayTime=20000
http://localhost:8080/rabbitmq/delayMsg2?msg=msg2&delayTime=2000
日志如下:
2019-07-28 17:28:13.729 INFO 25804 --- [nio-8080-exec-2] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 17:28:13 CST 2019,收到请求,msg:msg1,delayTime:20000
2019-07-28 17:28:20.607 INFO 25804 --- [nio-8080-exec-1] c.m.d.controller.RabbitMQMsgController : 当前时间:Sun Jul 28 17:28:20 CST 2019,收到请求,msg:msg2,delayTime:2000
2019-07-28 17:28:22.624 INFO 25804 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 17:28:22 CST 2019,延时队列收到消息:msg2
2019-07-28 17:28:33.751 INFO 25804 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer : 当前时间:Sun Jul 28 17:28:33 CST 2019,延时队列收到消息:msg1
第二个消息被优先消费,这符合预期。至此,利用 RabbitMQ 实现延时队列的方案介绍完毕。
九、总结
延时队列在需要延迟处理的场景中非常有用。利用 RabbitMQ 实现延时队列能够充分发挥其多项特性,例如消息的可靠发送与投递、通过死信队列保障消息至少被消费一次,以及确保未被正确处理的消息不会被丢弃。此外,借助 RabbitMQ 的集群特性,可以有效解决单点故障问题,避免因单个节点失效而导致延时队列不可用或消息丢失。
当然,实现延时队列还有多种其他选择,例如利用 Java 的 DelayQueue、Redis 的 zset、Quartz 任务调度框架或 Kafka 的时间轮机制。这些方法各有特点,可根据需求进行选择。





浙公网安备 33010602011771号