RabbitMQ TTL延迟队列以及问题解决

本文介绍了RabbitMQ使用TTL实现延迟队列的流程,包括配置绑定、死信队列监听消费者、消息接口等。然而,存在一个问题,即延迟消息可能不会按预期顺序执行。为了解决这个问题,文章推荐使用RabbitMQ的延迟队列插件,通过改变延迟机制来确保消息按预设延迟时间依次处理。并提供了下载、安装和配置插件的步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

流程图设计

        

 创建配置类进行绑定

/**
 * TTL 队列 配置文件类
 */
@Configuration
public class TtlQueueConfig {

    /**
     * 普通交换机的名称
     */
    private static final String X_EXCHANGE = "X";
    /**
     * 死信交换机的名称
     */
    private static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    /**
     * 普通队列的名称
     */
    private static final String QUEUE_A = "QA";
    private static final String QUEUE_B = "QB";
    /**
     * 死信队列的名称
     */
    private static final String DEAD_LETTER_QUEUE = "QD";

    /**
     * 声明X交换机
     */
    @Bean("xExchange")
    public DirectExchange xExchange() {
        return new DirectExchange(X_EXCHANGE);
    }

    /**
     * 声明Y交换机
     */
    @Bean("yExchange")
    public DirectExchange yExchange() {
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    /**
     * 声明队列 QA 绑定死信交换机Y routingKey=YD  TTL 过期时间10S
     */
    @Bean("queueA")
    public Queue queueA() {
        Map<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key", "YD");
        //设置TTL 单位是ms
        arguments.put("x-message-ttl", 10 * 1000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    /**
     * 声明队列 QB 绑定死信交换机Y routingKey=YD TTL 过期时间40S
     */
    @Bean("queueB")
    public Queue queueB() {
        Map<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key", "YD");
        //设置TTL 单位是ms
        arguments.put("x-message-ttl", 40 * 1000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    /**
     * 死信队列 QD
     */
    @Bean("queueD")
    public Queue queueD() {
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    /**
     * 队列绑定 QA和X交换机绑定 routingKey=XA
     */
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    /**
     * 队列绑定 QB和X交换机绑定 routingKey=XB
     */
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    /**
     * 队列绑定 QD和Y交换机绑定 routingKey=YD
     */
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

创建死信队列监听消费者

/**
 * 延迟队列TTL 消费者
 */
@Slf4j
@Component
public class DeadLetterQueueConsumer {

    /**
     * 监听死信队列QD 接收消息
     */
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信消息队列:{}", new Date().toString(), msg);
    }
}

创建简单的消息推送接口

/**
 * 返送延迟消息
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 消息推送接口
     *
     * @param message 推送消息
     */
    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message) {
        log.info("当前时间:{},发送一条信息给两个TTL队列:{}", new Date().toString(), message);
        //进行消息推送
        rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列:" + message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40s的队列:" + message);
    }

}

启动执行结果

调用接口

http://localhost:8080/ttl/sendMsg/娃哈哈

控制台打印结果

2021-07-24 15:01:40.714  INFO 38495 --- [nio-8080-exec-1] c.s.r.s.controller.SendMsgController     : 当前时间:Sat Jul 24 15:01:40 CST 2021,发送一条信息给两个TTL队列:娃哈哈
2021-07-24 15:01:50.733  INFO 38495 --- [ntContainer#0-1] c.s.r.s.c.DeadLetterQueueConsumer        : 当前时间:Sat Jul 24 15:01:50 CST 2021,收到死信消息队列:消息来自ttl为10s的队列:娃哈哈
2021-07-24 15:02:20.725  INFO 38495 --- [ntContainer#0-1] c.s.r.s.c.DeadLetterQueueConsumer        : 当前时间:Sat Jul 24 15:02:20 CST 2021,收到死信消息队列:消息来自ttl为40s的队列:娃哈哈

自定义延迟时间队列配置

   /**
     * 普通队列的名称
     */
    private static final String QUEUE_C = "QC";

    //声明QC
    @Bean("queueC")
    public Queue queueC() {
        Map<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key", "YD");
        //无需定义过期时间
        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }

    /**
     * 队列绑定 QC和X交换机绑定 routingKey=XC
     */
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

自定义时间消息接口

/**
     * 自定义时间消息推送接口
     */
    @GetMapping("/sendExpirationMsg/{message}/{ttlTime}")
    public void sendMsg(@PathVariable String message, @PathVariable String ttlTime) {
        log.info("当前时间:{},发送一条时长{}毫秒的信息给一个TTL队列:{}", new Date().toString(), ttlTime, message);
        //进行消息推送
        rabbitTemplate.convertAndSend("X", "XC", "消息来自ttl为" + ttlTime + "ms的队列:" + message, msg -> {
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }

启动执行结果

调用接口

http://localhost:8080/ttl/sendExpirationMsg/娃哈哈1/20000
http://localhost:8080/ttl/sendExpirationMsg/娃哈哈2/2000

控制台打印结果

2021-07-24 16:37:05.475  INFO 61239 --- [nio-8080-exec-4] c.s.r.s.controller.SendMsgController     : 当前时间:Sat Jul 24 16:37:05 CST 2021,发送一条时长20000毫秒的信息给一个TTL队列:娃哈哈1
2021-07-24 16:37:10.360  INFO 61239 --- [nio-8080-exec-5] c.s.r.s.controller.SendMsgController     : 当前时间:Sat Jul 24 16:37:10 CST 2021,发送一条时长2000毫秒的信息给一个TTL队列:娃哈哈2
2021-07-24 16:37:25.477  INFO 61239 --- [ntContainer#0-1] c.s.r.s.c.DeadLetterQueueConsumer        : 当前时间:Sat Jul 24 16:37:25 CST 2021,收到死信消息队列:消息来自ttl为20000ms的队列:娃哈哈1
2021-07-24 16:37:25.477  INFO 61239 --- [ntContainer#0-1] c.s.r.s.c.DeadLetterQueueConsumer        : 当前时间:Sat Jul 24 16:37:25 CST 2021,收到死信消息队列:消息来自ttl为2000ms的队列:娃哈哈2

存在问题

先后发送了两条消息,第一条延迟20s,第二条延迟2s,最终结果是同时到达。因为RabbitMQ只会检查第一条消息是否过期,过期就丢到死信队列进行排队,如果第一条消息延迟时间很长,那么之后的消息也不会得到优先执行。这个问题可以通过RabbitMQ插件得到解决。

消息延迟队列插件(解决上述问题)

下载地址:

https://www.rabbitmq.com/community-plugins.html

下载插件:

rabbitmq_delayed_message_exchange

复制插件到RabbitMQ目录下的plugins文件夹中:

/usr/local/Cellar/rabbitmq/3.7.16/plugins

安装插件并重启MQ:(提示started 1 plugins 表示安装成功 

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

登录后台可看见新增了一个x-delayed-message的交换机。

这样就把之前基于死信队列的消息延迟,变成了基于交换机的延迟。

延迟插件配置

/**
 * 延迟队列插件配置
 */
@Configuration
public class DelayedQueueConfig {

    /**
     * 交换机名称
     */
    private static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    /**
     * 队列名称
     */
    private static final String DELAYED_QUEUE_NAME = "delayed.queue";
    /**
     * routingKey
     */
    private static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    /**
     * 声明交换机
     */
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> arguments = new HashMap<>(2);
        arguments.put("x-delayed-type", BuiltinExchangeType.DIRECT);
        /*
         * 自定义交换机
         * 1.交换机的名称
         * 2.交换机的类型
         * 3.是否需要持久化
         * 4.是否需要自动删除
         * 5.其他参数
         */
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);
    }

    /**
     * 绑定交换机和队列
     */
    @Bean
    public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue,
                                                      @Qualifier("delayedExchange") CustomExchange delayedExchange) {
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

插件消息生产者

/**
     * 基于插件发送延迟消息
     */
    @GetMapping("/sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
        log.info("当前时间:{},发送一条时长{}毫秒的信息给一个延迟队列delayed.queue:{}", new Date().toString(), delayTime, message);
        //进行消息推送 延迟时长,单位ms
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message, msg -> {
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
    }

插件消息消费者

/**
 * 基于插件的延迟消息消费者
 */
@Slf4j
@Component
public class DelayQueueConsumer {

    /**
     * 监听延迟消息
     */
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayQueue(Message message) {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到延迟队列的消息:{}", new Date().toString(), msg);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值