一、延迟队列实现方式和优缺点
实现方式 | 优点 | 缺点 |
---|---|---|
DelayQueue | 1、效率高 2、任务触发时间延迟低 | 1、重启后数据丢失 2、内存限制,容易 OOM 3、集群扩展难度高 4、代码复杂度较高 |
Redis Zset | 1、由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。 2、做集群扩展相当方便 3、时间准确度高 | 1、高并发条件下,多消费者会取到同一个订单号;用分布式锁,但是用分布式锁,性能下降了。对ZREM的返回值进行判断,只有大于0的时候,才消费数据。 2、需要额外进行 Redis 维护 |
RabbitMQ TTL+死信队列 | 1、高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。 | 1、本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高 |
RabbitMQ插件 | 相比上面方式,实现简单 |
二、JDK
延迟队列DelayQueue
实现
实现步骤:
- 实现接口
Delayed
,重写方法getDelay
和CompareTo
- 通过延迟队列类
DelayQueue
生产和消费实现类 - 测试
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实现原理](https://i-blog.csdnimg.cn/blog_migrate/70146e0088276763ad22492b474d6aca.png)
三、基于 Redis
的ZSet
实现
实现原理:
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安装教程
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-exchange
和x-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+私信队列实现延迟队列原理](https://i-blog.csdnimg.cn/blog_migrate/c7405adbbd06f07e884d7f4b29b735ae.png)
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秒再从队列中出队,各个消息的出队顺序互不影响,且延迟时间准确。
总体来说,该插件不需要借助死信队列,就可以实现延迟队列的效果,相对于通过死信队列设置队列超时和消息超时的方式来说更加简单,而且效果更加完美。
参考: