RabbitMQ 延迟队列

原文(部分调整):【RabbitMQ】一文带你搞定 RabbitMQ 延迟队列

一、说明

上一篇文章详细探讨了 RabbitMQ 中的死信队列,包括其概念、使用场景及实现方法。若读者尚未了解死信队列,建议先行阅读该文章,因为本文内容与死信队列紧密相关。

本文将继续深入探讨 RabbitMQ 的高级特性,通过学习,读者将了解到:

  1. 什么是延时队列
  2. 延时队列使用场景
  3. RabbitMQ 中的 TTL
  4. 如何利用 RabbitMQ 来实现延时队列

二、本文大纲

本文大纲如下:

本文大纲

在阅读本文前,建议读者对 RabbitMQ 及其死信队列有基本了解。

三、什么是延时队列

延时队列首先是一种队列。队列的特点是其内部元素有序,且元素的出队和入队具有明确的方向性,即元素从一端进入,从另一端取出。

其次,延时队列最显著的特性体现在其延时属性上。与普通队列不同,普通队列中的元素通常期望被尽快取出并处理,而延时队列中的元素则期望在指定时间点被取出和处理。因此,延时队列中的元素都带有时间属性,通常代表需要在未来某个时间点执行的消息或任务。

简而言之,延时队列是用于存放需要在指定时间点被处理的元素的队列。

四、延时队列使用场景

那么,延时队列适用于哪些场景呢?可以考虑以下情况:

  1. 订单在 10 分钟内未支付则自动取消。
  2. 新创建的店铺,如果在 10 天内未上传商品,则自动发送消息提醒。
  3. 账单在 1 周内未支付,则自动结算。
  4. 用户注册成功后,如果 3 天内未登录则进行短信提醒。
  5. 用户发起退款,如果 3 天内未得到处理则通知相关运营人员。
  6. 预定会议后,需要在预定时间点前 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 的管理后台,可以看到我们刚才创建的交换机和队列信息:

RabbitMQ Exchange 信息
RabbitMQ Queues 信息 1
RabbitMQ Queues 信息 2

接下来,我们发送几条消息,例如访问以下 URL:http://localhost:8080/rabbitmq/sendmsg?msg=testMsg1&delayType=1http://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 的时间轮机制。这些方法各有特点,可根据需求进行选择。

posted @ 2025-12-11 15:18  Higurashi-kagome  阅读(0)  评论(0)    收藏  举报