延迟队列的实现

一、延迟队列实现方式和优缺点

实现方式优点缺点
DelayQueue1、效率高
2、任务触发时间延迟低
1、重启后数据丢失
2、内存限制,容易OOM
3、集群扩展难度高
4、代码复杂度较高
Redis Zset1、由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。
2、做集群扩展相当方便
3、时间准确度高
1、高并发条件下,多消费者会取到同一个订单号;用分布式锁,但是用分布式锁,性能下降了。对ZREM的返回值进行判断,只有大于0的时候,才消费数据。
2、需要额外进行 Redis 维护
RabbitMQ TTL+死信队列1、高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。1、本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高
RabbitMQ插件相比上面方式,实现简单

二、JDK延迟队列DelayQueue 实现

实现步骤:

  1. 实现接口 Delayed,重写方法 getDelayCompareTo
  2. 通过延迟队列类 DelayQueue 生产和消费实现类
  3. 测试

2.1 实现代码

DiyDelayed.java
package com.zoro.delayqueue.jdk;

import org.jetbrains.annotations.NotNull;

import java.io.Serializable;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author zoro
 */
public class DiyDelayed implements Delayed, Serializable {

    private String diyName;

    private long delayTimeMillis;

    private long createTimeMillis;

    @Override
    public long getDelay(@NotNull TimeUnit unit) {
        return unit.convert(getEndTimeMillis() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(@NotNull Delayed delayed) {
        DiyDelayed diyDelayed = (DiyDelayed) delayed;
        return (int) ((getEndTimeMillis() - diyDelayed.getEndTimeMillis()));
    }

    public DiyDelayed() {
    }

    public DiyDelayed(String diyName, long delayTimeMillis) {
        this.diyName = diyName;
        this.delayTimeMillis = delayTimeMillis;
        this.createTimeMillis = System.currentTimeMillis();
    }

    public long getDelayTimeMillis() {
        return delayTimeMillis;
    }

    public long getEndTimeMillis() {
        return this.createTimeMillis + this.delayTimeMillis;
    }

    public String getDiyName() {
        return diyName;
    }

    public void setDiyName(String diyName) {
        this.diyName = diyName;
    }
}

ProducerDelayQueueThread.java
package com.zoro.delayqueue.jdk;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author zoro
 */
public class ProducerDelayQueueThread extends Thread {

    private Logger logger = LoggerFactory.getLogger(ProducerDelayQueueThread.class);

    private DiyDelayService resource;

    public ProducerDelayQueueThread(DiyDelayService resource) {
        this.resource = resource;
    }

    public void run() {
        logger.info("延迟队列生产者线程启动");
        for (int i = 2; i >= 1; i--) {
            try {
                // 设置到期时间 100s
                DiyDelayed diyDelayed = new DiyDelayed(String.valueOf(i), Long.parseLong(String.valueOf(i * 10 * 1000)));
                resource.produce(diyDelayed);
                logger.info("生产者{}开始生产!", i);
            } catch (Exception e) {
                logger.error("延迟队列生产异常", e);
            }
        }
    }
}

ConsumerDelayQueueThread.java
package com.zoro.delayqueue.jdk;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * @author zoro
 */
public class ConsumerDelayQueueThread extends Thread {

    private Logger logger = LoggerFactory.getLogger(ConsumerDelayQueueThread.class);

    private DiyDelayService resource;

    public ConsumerDelayQueueThread(DiyDelayService resource) {
        this.resource = resource;
    }

    public void run() {
        logger.info("延迟队列消费者线程启动");
        for (int i = 0; i < 2; i++) {
            try {
                resource.consume();
            } catch (Exception e) {
                logger.error("延迟队列消费异常", e);
            }
        }
    }
}
DiyDelayService.java
package com.zoro.delayqueue.jdk;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.DelayQueue;

/**
 * 延迟队列生产消费类
 *
 * @author zoro
 */
public class DiyDelayService {

    private static Logger logger = LoggerFactory.getLogger(DiyDelayService.class);

    private DelayQueue<DiyDelayed> delayQueue = new DelayQueue<>();

    /**
     * 生产者
     */
    public void produce(DiyDelayed diyDelayed) {
        try {
            delayQueue.put(diyDelayed);
        } catch (Exception e) {
            logger.error("生产异常", e);
        }
    }

    /**
     * 消费者
     * take方法 未到期阻塞
     */
    public void consume() {
        try {
            DiyDelayed diyDelayed = delayQueue.take();
            logger.info("任务{}, 时间已过{}s, 开始处理业务逻辑!!!", diyDelayed.getDiyName(), diyDelayed.getDelayTimeMillis() / 1000);
            // 真正的业务逻辑触发……

        } catch (Exception e) {
            logger.error("消费异常", e);
        }
    }

}
DiyDelayedTest.java
package com.zoro.delayqueue.jdk;

/**
 * @author zoro
 */
public class DiyDelayedTest {

    public static void main(String[] args) {
        DiyDelayService resource = new DiyDelayService();
        // 创建一个生产者线程
        ProducerDelayQueueThread p = new ProducerDelayQueueThread(resource);
        // 目前只开启一个生产者线程和一个消费者线程,后续可以改成线程池
        ConsumerDelayQueueThread c = new ConsumerDelayQueueThread(resource);
        // start 生产者和消费者线程,开始工作
        p.start();
        c.start();
    }
}
运行结果:
Connected to the target VM, address: '127.0.0.1:52201', transport: 'socket'
10:34:15.376 [Thread-0] INFO com.zoro.delayqueue.jdk.ProducerDelayQueueThread - 延迟队列生产者线程启动
10:34:15.376 [Thread-1] INFO com.zoro.delayqueue.jdk.ConsumerDelayQueueThread - 延迟队列消费者线程启动
10:34:15.380 [Thread-0] INFO com.zoro.delayqueue.jdk.ProducerDelayQueueThread - 生产者2开始生产!
10:34:15.381 [Thread-0] INFO com.zoro.delayqueue.jdk.ProducerDelayQueueThread - 生产者1开始生产!
10:34:25.396 [Thread-1] INFO com.zoro.delayqueue.jdk.DiyDelayService - 任务1, 时间已过10s, 开始处理业务逻辑!!!
10:34:35.381 [Thread-1] INFO com.zoro.delayqueue.jdk.DiyDelayService - 任务2, 时间已过20s, 开始处理业务逻辑!!!
Disconnected from the target VM, address: '127.0.0.1:52201', transport: 'socket'

Process finished with exit code 0

2.2 原理

 通过实现类Delayed 设置延迟时间和优先队列中比较方法, 延迟队列DelayQueue 添加Delayed实现类,实际上是添加到优先队列PriorityQueue中,PriorityQueue 的方法 offer 内部通过Delayed实现类方法CompareTo实现优先次序;消费的过程中按照此优先次序进行消费,队列 take 方法在延迟时间未到时阻塞等待,从而实现延迟的作用。

源码流程图如下:
DelayQueue实现原理

三、基于 RedisZSet 实现

实现原理:

zadd 添加消息

zrangebyscore 通过score 排序,获取第一个消息

3.1 实现代码

RedisConnectionUtil.java
package com.zoro.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis 初始化
 *
 * @author zoro
 * @date 2023/6/6
 */
public class RedisConnectionUtil {

    private static JedisPool jedisPool = null;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        jedisPool = new JedisPool(config, "127.0.0.1", 6379);
    }

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }

    public static void main(String[] args) {
        Jedis jedis = RedisConnectionUtil.getJedis();
        jedis.set("test", "test2");
    }
}

DelayQueue.java
package com.zoro.redis;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import redis.clients.jedis.Jedis;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;

/**
 * Redis 实现延时队列
 * <p>
 * zset实现:
 * zadd 添加消息
 * zrangebyscore 通过score 排序,获取第一个消息
 * <p>
 * zrem 删除消息
 *
 * @author zoro
 * @date 2023/6/5 10:12
 */
public class DelayQueue<T> {

    static class TaskMsg<T> {
        private String msgId;
        private T msg;

        public String getMsgId() {
            return msgId;
        }

        public void setMsgId(String msgId) {
            this.msgId = msgId;
        }

        public T getMsg() {
            return msg;
        }

        public void setMsg(T msg) {
            this.msg = msg;
        }
    }

    private Jedis jedis;

    private String queueKey;

    private Type taskType = new TypeReference<TaskMsg<T>>() {
    }.getType();

    public DelayQueue(Jedis jedis, String queueKey) {
        this.jedis = jedis;
        this.queueKey = queueKey;
    }

    /**
     * 延时队列 入队消息
     *
     * @param msg        消息内容
     * @param delayTimes 延时时间
     */
    public void produce(T msg, int delayTimes) {
        TaskMsg<T> taskMsg = new TaskMsg<>();
        String uuid = UUID.randomUUID().toString();
        taskMsg.setMsgId(uuid);
        taskMsg.setMsg(msg);

        //序列化消息
        String info = JSON.toJSONString(taskMsg);
        jedis.zadd(queueKey, System.currentTimeMillis() + delayTimes, info);
    }

    public void consume() {
        while (!Thread.interrupted()) {
            //System.out.println("start ^^^^^^^^^^");
            // 获取一条
            Set<String> taskSet = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
            if (taskSet.isEmpty()) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            String msg = taskSet.iterator().next();
            Long msgL = jedis.zrem(queueKey, msg);
            if (msgL > 0) {
                TaskMsg<String> taskMsg = JSON.parseObject(msg, taskType);
                handlerMsg(taskMsg.getMsg());
            }
        }
    }

    public void handlerMsg(String msg) {
        System.out.println(Thread.currentThread().getName() + " consumer msg: " + msg);
    }


    public static void main(String[] args) throws InterruptedException, IOException {

        DelayQueue<String> delayQueue = new DelayQueue<>(RedisConnectionUtil.getJedis(), "delayQueue_1");
        //producer
        Thread producerT = new Thread(() -> {
            for (int i = 4; i > 0; i--) {
                delayQueue.produce("第" + i + "条小鱼游过来了!!!", i * 5000);
            }
        });
        //consumer
        Thread consumerT = new Thread(() -> {
            delayQueue.consume();
        });

        producerT.start();
        producerT.join();
        Thread.sleep(6 * 1000);

        consumerT.start();
        consumerT.join();
        consumerT.interrupt();

        System.in.read();
    }

}

四、基于 RabbitMQ 实现

引用如下文章,安装 RabbitMQ 以及延迟插件

RabbitMQ 安装:RabbitMQ+docker安装教程

Docker 安装 RabbitMQ 并安装延迟队列插件

4.1 通过RabbitMQ的高级特性TTL和死信队列实现延迟队列

原理: 如支付订单消息进入 Rabbit 业务队列并设置超时时间,业务队列不配置消费者,超时消息被放进私信队列,私信队列消费者消费此消息,可以取消此订单或者其他操作。

实现代码
application.yml
server:
  port: 8080
spring:
  application:
    name: rabbitmq-server
  #配置rabbitMq 服务器
  rabbitmq:
    host: 192.168.10.128
    port: 5672
    username: admin
    password: admin
    listener:
      simple:
        acknowledge-mode: manual  # 消息确认方式,其有三种配置方式,分别是none、manual(手动ack) 和auto(自动ack) 默认auto
        retry:
          enabled: true
          max-attempts: 3
          max-interval: 600000   # 重试最大间隔时间10分钟
          initial-interval: 600000  # 重试初始间隔时间10分钟
          multiplier: 1
        default-requeue-rejected: false
    virtual-host: / # 不同的用户可以设置不同的 virtual-host,默认是/
RabbitConfig.java
package com.example.zororabbitmq.delaymq_dlq;

import lombok.Data;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * 延迟队列配置
 *
 * @author zoro
 */
@Configuration
@Data
public class RabbitConfig {

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private int port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;

    /**
     * 业务队列交换机
     */
    public static final String BUSINESS_EXCHANGE = "zoro.business.exchange";

    /**
     * 业务队列routingKey
     */
    public static final String BUSINESS_ROUTING_KEY = "zoro.001";

    /**
     * 业务队列
     */
    public static final String BUSINESS_QUEUE = "zoro.business.queue";

    /**
     * 死信队列交换机
     */
    public static final String DEAD_LETTER_EXCHANGE = "zoro.dead.letter.exchange";
    /**
     * 死信队列routingKey
     */
    public static final String DEAD_LETTER_ROUTING_KEY = "zoro.dead.letter.001";
    /**
     * 死信队列
     */
    public static final String DEAD_LETTER_QUEUE = "zoro.dead.letter.queue";

    /**
     * 声明业务队列的交换机
     */
    @Bean("businessExchange")
    public DirectExchange businessExchange() {
        return new DirectExchange(BUSINESS_EXCHANGE);
    }

    /**
     * 声明死信交换机
     */
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    /**
     * 声明业务队列
     */
    @Bean("businessQueue")
    public Queue businessQueue() {
        Map<String, Object> args = new HashMap<>(16);
        // 设置当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // 设置当前队列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY);
        // 设置消息的过期时间 单位:ms(毫秒)
        args.put("x-message-ttl", 5000);
        return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
    }

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

    /**
     * 声明业务队列和业务交换机的绑定关系
     */
    @Bean
    public Binding businessBinding(@Qualifier("businessQueue") Queue queue,
                                   @Qualifier("businessExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(BUSINESS_ROUTING_KEY);
    }

    /**
     * 声明死信队列和死信交换机的绑定关系
     */
    @Bean
    public Binding deadLetterBinding(@Qualifier("deadLetterQueue") Queue queue,
                                     @Qualifier("deadLetterExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_ROUTING_KEY);
    }
}

配置业务交换机、业务队列、死信交换机、死信队列,并通过路由键将业务交换机和业务队列绑定在一起,将死信交换机和死信队列绑定在一起。

特别注意,声明业务队列时,指定了两个参数x-dead-letter-exchangex-dead-letter-routing-key,如果不配置这两个参数,那么在业务消息消费失败之后,是不会投递到死信交换机。

通过参数x-message-ttl配置了消息的过期时间(在声明队列时指定消息的过期时间,对当前队列中所有的消息都有效)。

Order.java
package com.example.zororabbitmq.delaymq_dlq;

/**
 * @author zoro
 * @date 2023-06-14 10:48:44
 */
public class Order {

    private String id;

    private String itemName;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    @Override
    public String toString() {
        return "Order{" +
                "id='" + id + '\'' +
                ", itemName='" + itemName + '\'' +
                '}';
    }
}

MsgProducer.java
package com.example.zororabbitmq.delaymq_dlq;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
@Slf4j
public class MsgProducer {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void send(Order order) {
        log.info("开始发送业务消息");
        rabbitTemplate.convertAndSend(RabbitConfig.BUSINESS_EXCHANGE, RabbitConfig.BUSINESS_ROUTING_KEY,
                JSON.toJSONString(order));
    }
}

上面生产者是发送消息到业务队列,但是注意不需要为该业务队列配置消费者,不然消息在过期之前可能就被消费者消费了!也就没法实现延迟队列的效果了

MsgConsumer.java
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 监听死信队列消息
 */
@Component
@Slf4j
public class MsgConsumer {
    @RabbitListener(queues = RabbitConfig.DEAD_LETTER_QUEUE)
    public void receiveA(Message message, Channel channel) throws IOException {
        Order order = JSON.parseObject(message.getBody(), Order.class);
        log.info("收到消息:{}", order.toString());
        //log.info("死信消息附带的头信息: {}", JSON.toJSONString(message.getMessageProperties().getHeaders()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

消费者监听并消费死信队列消息,我们业务场景再做业务需要操作。

TestController.java
package com.example.zororabbitmq.delaymq_dlq;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("test")
public class TestController {

    @Resource
    private MsgProducer businessProducer;

    @RequestMapping(value = "sendMsg")
    public String sendMsg() {
        for (int i = 1; i <= 3; i++) {
            Order order = new Order();
            order.setId("2023061400000" + i);
            order.setItemName("索隆手办001" + "_" + i);
            businessProducer.send(order);
        }
        return "success";
    }
}
运行结果
2023-06-14 17:38:11.459  INFO 12168 --- [nio-8080-exec-1] c.e.z.delaymq_dlq.MsgProducer            : 开始发送业务消息
2023-06-14 17:38:11.482  INFO 12168 --- [nio-8080-exec-1] c.e.z.delaymq_dlq.MsgProducer            : 开始发送业务消息
2023-06-14 17:38:11.482  INFO 12168 --- [nio-8080-exec-1] c.e.z.delaymq_dlq.MsgProducer            : 开始发送业务消息
2023-06-14 17:38:16.488  INFO 12168 --- [ntContainer#0-1] c.e.z.delaymq_dlq.DeadLetterConsumer     : 收到消息:Order{id='20230614000001', itemName='索隆手办001_1'}
2023-06-14 17:38:16.489  INFO 12168 --- [ntContainer#0-1] c.e.z.delaymq_dlq.DeadLetterConsumer     : 收到消息:Order{id='20230614000002', itemName='索隆手办001_2'}
2023-06-14 17:38:16.490  INFO 12168 --- [ntContainer#0-1] c.e.z.delaymq_dlq.DeadLetterConsumer     : 收到消息:Order{id='20230614000003', itemName='索隆手办001_3'}

从上面的接口看,延迟时间确实是我们设置的5000ms。

原理流程图:
RabbitMQ TTL+私信队列实现延迟队列原理

4.2 通过rabbitmq_delayed_message_exchange插件实现延迟队列

实现代码
RabbitConfig2.java
package com.example.zororabbitmq.delaymq_plugins;

import lombok.Data;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * 延迟队列配置
 *
 * @author zoro
 */
@Configuration
@Data
public class RabbitConfig2 {

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private int port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;

    /**
     * 延迟队列routingKey
     */
    public static final String DELAY_ROUTING_KEY = "zoro.delay.001";

    /**
     * 延迟交换机
     */
    public static final String DELAY_EXCHANGE = "delay.exchange";

    /**
     * 延迟队列
     */
    public static final String DELAY_QUEUE = "delay.queue";


    /**
     * 声明延迟交换机
     */
    @Bean("delayExchange")
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>(1);
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", false, false, args);
    }

    /**
     * 声明业务队列
     */
    @Bean("delayQueue")
    public Queue delayQueue() {
        return QueueBuilder.durable(DELAY_QUEUE).build();
    }


    /**
     * 声明延迟队列和延迟交换机的绑定关系
     */
    @Bean
    public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
        return BindingBuilder.bind(delayQueue).to(delayExchange).with(DELAY_ROUTING_KEY).noargs();
    }
}

配置延迟交换机、延迟队列,并将延迟交换机和延迟队列绑定。

定义一个类型为x-delayed-message交换机,这个类型不是RabbitMQ默认的那几种交换机类型之一,而是我们上面通过插件安装的。

然后,通过x-delayed-type参数指定该交换机的类型是direct类型。

MsgProducer2.java
package com.example.zororabbitmq.delaymq_plugins;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 延迟队列消息生产者
 *
 * @author zoro
 */
@Component
@Slf4j
public class MsgProducer2 {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void send(String msg, Integer delaySeconds) {
        log.info("开始发送业务消息:{}, 延时时间:{} 秒,发送时间:{}", msg, delaySeconds,
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        MessagePostProcessor messagePostProcessor = message -> {
            message.getMessageProperties().setDelay(delaySeconds * 1000);
            return message;
        };
        rabbitTemplate.convertAndSend(RabbitConfig2.DELAY_EXCHANGE, RabbitConfig2.DELAY_ROUTING_KEY, msg, messagePostProcessor);
    }
}

发送消息时,在消息的通过 setDelay() 中添加x-delay参数指定延迟时间。

注意,这里不同的版本设置延迟时间的方法不一致,自行判断使用!

MsgConsumer2.java
package com.example.zororabbitmq.delaymq_plugins;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 监听延迟队列消息
 *
 * @author zoro
 */
@Component
@Slf4j
public class MsgConsumer2 {
    @RabbitListener(queues = RabbitConfig2.DELAY_QUEUE)
    public void receiveA(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("收到消息:{},当前时间:{}", msg, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("消息消费失败:{}", e.getMessage());
            // 消息消费异常后,返回一个nack响应
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

通过@RabbitListener注解指定消费队列。

TestController2.java
package com.example.zororabbitmq.delaymq_plugins;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("test2")
public class TestController2 {

    @Resource
    private MsgProducer2 msgProducer2;

    /**
     * 延迟队列测试
     *
     * @return
     * @throws InterruptedException
     */
    @RequestMapping(value = "sendMsg")
    public String sendMsg() throws InterruptedException {
        for (int i = 1; i <= 3; i++) {
            msgProducer2.send("第" + i + "条小鱼游过来了!", 5);
            Thread.sleep(2 * 1000);
        }
        return "success";
    }
}
运行结果:
2023-06-29 14:40:41.058  INFO 12728 --- [nio-8080-exec-1] c.e.z.delaymq_plugins.MsgProducer2       : 开始发送业务消息:第1游过来了!, 延时时间:5 秒,发送时间:2023-06-29 14:40:41
2023-06-29 14:40:43.100  INFO 12728 --- [nio-8080-exec-1] c.e.z.delaymq_plugins.MsgProducer2       : 开始发送业务消息:第2游过来了!, 延时时间:5 秒,发送时间:2023-06-29 14:40:43
2023-06-29 14:40:45.113  INFO 12728 --- [nio-8080-exec-1] c.e.z.delaymq_plugins.MsgProducer2       : 开始发送业务消息:第3游过来了!, 延时时间:5 秒,发送时间:2023-06-29 14:40:45
2023-06-29 14:40:46.128  INFO 12728 --- [ntContainer#1-1] c.e.z.delaymq_plugins.MsgConsumer2       : 收到消息:第1游过来了!,当前时间:2023-06-29 14:40:46
2023-06-29 14:40:48.106  INFO 12728 --- [ntContainer#1-1] c.e.z.delaymq_plugins.MsgConsumer2       : 收到消息:第2游过来了!,当前时间:2023-06-29 14:40:48
2023-06-29 14:40:50.117  INFO 12728 --- [ntContainer#1-1] c.e.z.delaymq_plugins.MsgConsumer2       : 收到消息:第3游过来了!,当前时间:2023-06-29 14:40:50

从上面的打印结果可以看出,虽然生产者投递的消息需要延时5秒再从队列中出队,各个消息的出队顺序互不影响,且延迟时间准确。

总体来说,该插件不需要借助死信队列,就可以实现延迟队列的效果,相对于通过死信队列设置队列超时和消息超时的方式来说更加简单,而且效果更加完美。

参考:

项目中五种延迟队列的实现及优缺点

《RabbitMQ系列》之RabbitMQ的延迟队列(Lazy Queues)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值