RabbitMq中一看就懂的延时队列实现

105 阅读3分钟

RabbitMq本身是没有延时队列的,它自身是不支持的,但是可以通过别的途径来实现,比如插件或者死信队列(DLX)来实现延迟队列的功能。

首先来说不需要安装插件,使用死信队列的方式来模拟达到延时的效果。具体的原理是:

  1. 定义一个普通的队列作为延时队列delay-queue-test, 然后设置消息的过期时间。
  2. 这个延时队列并没有消费者去消费,主动等待消息过期,之后将消息路由到死信队列
  3. 消费者从死信队列中获取消息,然后消费,从而达到延时的效果。

具体的实现步骤:

配置死信队列和延时队列

package com.im.chat.mq.config;
​
import com.im.chat.message.constants.QueueConst;
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.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @Title: RabbitmqDelayConfig
 * @Package com.im.chat.mq.config
 * @Description:
 */
@Configuration
public class RabbitmqDelayConfig {
​
    // 定义延时队列
    @Bean
    public Queue delayedQueue() {
        return QueueBuilder.durable(QueueConst.DeadLetter.QUEUE_DELAY_30000)
                .withArgument("x-dead-letter-exchange", QueueConst.DIRECT_DELAY_EXCHANGE) // 死信交换机
                .withArgument("x-dead-letter-routing-key", QueueConst.DeadLetter.ROUTE_KEY_ORDER) // 死信路由键
                .withArgument("x-message-ttl", 30000) // 消息 TTL(单位:毫秒)
                .build();
    }
​
    // 定义死信队列
    @Bean
    public Queue deadLetterQueue() {
        return new Queue(QueueConst.DeadLetter.QUEUE_ORDER, true); // 持久化队列
    }
​
    // 定义死信交换机
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(QueueConst.DIRECT_DELAY_EXCHANGE, true, false);
    }
​
    // 绑定死信队列到死信交换机
    @Bean
    public Binding bindingDeadLetterQueue(Queue deadLetterQueue, @Qualifier("deadLetterExchange") DirectExchange deadLetterExchange) {
        return BindingBuilder.bind(deadLetterQueue)
                .to(deadLetterExchange)
                .with(QueueConst.DeadLetter.ROUTE_KEY_ORDER);
    }
}

发送消息到延时队列

package com.im.chat.mq.producer;
​
import com.im.chat.message.constants.QueueConst;
import com.im.chat.mq.TestMsgRecord;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
​
/**
 * @Title: DelayedMessageProducer
 * @Package com.im.chat.mq.producer
 * @Description:
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class DelayedMessageProducer {
​
    private final RabbitTemplate rabbitTemplate;
​
    public void sendDelayedMessage(String message) {
        log.info("发送消息:{}", message);
        TestMsgRecord testMsgRecord = new TestMsgRecord(message);
        rabbitTemplate.convertAndSend(QueueConst.DeadLetter.QUEUE_DELAY_30000, testMsgRecord);
    }
}

消费死信队列的消息

package com.im.chat.mq.consumer;
​
import com.fasterxml.jackson.databind.ObjectMapper;
import com.im.chat.message.constants.QueueConst;
import com.im.chat.mq.TestMsgRecord;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
​
import java.io.IOException;
​
/**
 * @Title: DeadLetterQueueConsumer
 * @Package com.im.chat.mq.consumer
 * @Description:
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class DeadLetterQueueConsumer {
​
    private final ObjectMapper objectMapper;
​
    @RabbitListener(queues = QueueConst.DeadLetter.QUEUE_ORDER)
    public void onMessage(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
​
        TestMsgRecord testMsgRecord = objectMapper.readValue(message.getBody(), TestMsgRecord.class);
        log.info("死信队列消费消息 {}", testMsgRecord);
​
        channel.basicAck(tag, false);
    }
}

image-20241210145123036.png

image-20241210145157912.png


下面是安装插件来实现延时队列的效果

具体步骤安装步骤

  1. 去Rabbitmq官网搜索rabbitmq-delayed-message-exchange 这个插件 rabbitmq-delayed-message-exchange, 根据安装的Rabbitmq版本找到对应的版本

image-20241210151121174.png

  1. 将下载的插件解压放入Rabbitmq的插件目录。比如 /usr/lib/rabbitmq/lib/rabbitmq_server-<version>/plugins

  2. 命令行启用插件 rabbitmq-plugins enable rabbitmq_delayed_message_exchange

  3. 重启Rabbitmq让其生效 systemctl restart rabbitmq-server

定义延时交换机

package com.im.chat.mq.config;
​
import com.im.chat.message.constants.QueueConst;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @Title: RabbitmqDelayConfig
 * @Package com.im.chat.mq.config
 * @Description:
 */
@Configuration
public class RabbitmqDelayPluginsConfig {
​
    // 定义延时交换机
    @Bean(value = "delayedPluginsExchange")
    public DirectExchange delayedPluginsExchange() {
        return ExchangeBuilder
          .directExchange(QueueConst.DIRECT_DELAY_PLUGINS_EXCHANGE)
          .delayed()
          .durable(true).build();
    }
​
    // 绑定队列到延时交换机
    @Bean
    public Declarables bindOrderAdvance(@Qualifier("delayedPluginsExchange") Exchange delayedPluginsExchange) {
        Queue queue = new Queue(QueueConst.DelayOrder.QUEUE, true, false, false, null);
        Binding binding = BindingBuilder.bind(queue).to(delayedPluginsExchange).with(QueueConst.DelayOrder.ROUTE_KEY).noargs();
        return new Declarables(queue, binding);
    }
}

发送延时消息

public class TestDelayMsg {
​
    private String msg;
​
    private String time;
​
    private Integer expired;
​
    public TestDelayMsg() {
    }
​
    public TestDelayMsg(String msg, String time, Integer expired) {
        this.expired = expired;
        this.msg = msg;
        this.time = time;
    }
​
    public Integer getExpired() {
        return expired;
    }
​
    public void setExpired(Integer expired) {
        this.expired = expired;
    }
​
    public String getMsg() {
        return msg;
    }
​
    public void setMsg(String msg) {
        this.msg = msg;
    }
​
    public String getTime() {
        return time;
    }
​
    public void setTime(String time) {
        this.time = time;
    }
​
    @Override
    public String toString() {
        return "TestDelayMsg{" +
                "expired=" + expired +
                ", msg='" + msg + ''' +
                ", time='" + time + ''' +
                '}';
    }
}
​
public void sendDelayedPluginsMessage(String message, int num) {
        log.info("发送消息:{}", message);
        String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        TestDelayMsg testMsgRecord = new TestDelayMsg(message, format, 1000 * num);
        HashMap<String, Object> map = new HashMap<>(3);
        map.put("x-delay", 1000 * num);
        map.put("x-retryCount", 3);
        map.put("x-timestamp", format);
        rabbitTemplate.convertAndSend(QueueConst.DIRECT_DELAY_PLUGINS_EXCHANGE, QueueConst.DelayOrder.ROUTE_KEY,
                testMsgRecord, m -> {
            map.forEach((k, v) -> m.getMessageProperties().setHeader(k, v));
            return m;
        }, new CorrelationData());
    }

消费者监听

package com.im.chat.mq.consumer;
​
import com.fasterxml.jackson.databind.ObjectMapper;
import com.im.chat.message.constants.QueueConst;
import com.im.chat.mq.TestDelayMsg;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
​
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
​
/**
 * @Title: DelayQueueConsumer
 * @Package com.im.chat.mq.consumer
 * @Description:
 */@RequiredArgsConstructor
@Component
@Slf4j
public class DelayQueueConsumer {
​
    private final ObjectMapper objectMapper;
​
    @RabbitListener(queues = QueueConst.DelayOrder.QUEUE)
    public void onMessage(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
​
        try {
            TestDelayMsg testMsgRecord = objectMapper.readValue(message.getBody(), TestDelayMsg.class);
​
            MessageProperties messageProperties = message.getMessageProperties();
​
            Map<String, Object> headers = messageProperties.getHeaders();
​
//            headers.forEach((k, v) -> {
//                log.info("消息头信息 key: {}, value: {}", k, v);
//            });
            log.info("延迟队列: 消费时间{} 消费消息 {}", LocalDateTime.now(), testMsgRecord);
​
            channel.basicAck(tag, false);
        } catch (Exception e) {
            channel.basicNack(tag, false, false);
            log.error("MQ消费失败", e);
        }
    }
}

image-20241211140657503.png

image-20241211140733686.png