7 延迟队列
7.1 概念
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
7.2 使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
7.3 springboot整合rabbitmq
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring.rabbitmq.host=192.168.29.101
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
7.4 队列TTL
7.4.1 案例
/**
* ttl队列 配置文件
*/
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X_EXCHANGE = "X";
//死信交换机名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//普通队列名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
//死信队列名称
public 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);
}
//声明队列 普通队列 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 =10s
arguments.put("x-message-ttl", 10000);
return QueueBuilder
.durable(QUEUE_A)
.withArguments(arguments)
.build();
}
//声明队列 普通队列 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 =10s
arguments.put("x-message-ttl", 40000);
return QueueBuilder
.durable(QUEUE_B)
.withArguments(arguments)
.build();
}
//死信队列
@Bean("queueD")
public Queue queueD() {
return QueueBuilder
.durable(DEAD_LETTER_QUEUE)
.build();
}
//绑定 queueA--XExchange
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueA)
.to(xExchange)
.with("XA");
}
//绑定queueB--XExchange
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueB)
.to(xExchange)
.with("XB");
}
//绑定queueD--YExchange
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange) {
return BindingBuilder
.bind(queueD)
.to(yExchange)
.with("YD");
}
}
/**
* 发送延迟消息
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable("message") String msg) {
log.info("当前时间:{},发送一条消息给两个ttl队列:{}", new Date(), msg);
//1:交换机名称 2:routingKey 3:消息
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+msg);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+msg);
}
}
/**
* ttl 消费者
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//接收消息
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列的消息:{}", new Date(), msg);
}
}
7.4.2 案例优化
不足:以上两个普通队列消息延迟分别为10s和40s,都已经耦合死;如果在增加一个60s的队列就需要再次单独增加。
解决:单独增加一个队列QC,不设置过期时间
//不设置过期时间 可扩展性队列
public static final String QUEUE_C = "QC";
//声明可扩展性队列 C
@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();
}
//绑定queueC--XExchange
@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 sendExpirationMsg(@PathVariable String message, @PathVariable String ttlTime) {
log.info("当前时间:{},发送一条时长:{}ms的消息给ttl队列QC,消息为:{}", new Date(), ttlTime, message);
rabbitTemplate.convertAndSend("X", "XC", message, (msg) -> {
//设置消息发送的过期时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
出现的问题:
发送
http://localhost:8080/ttl/sendExpirationMsg/你好1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好2/2000
2022-11-27 12:58:24.135 INFO 64000 --- [nio-8080-exec-3] c.w.r.controller.SendMsgController : 当前时间:Sun Nov 27 12:58:24 CST 2022,发送一条时长:20000ms的消息给ttl队列QC,消息为:你好1
2022-11-27 12:58:29.576 INFO 64000 --- [nio-8080-exec-4] c.w.r.controller.SendMsgController : 当前时间:Sun Nov 27 12:58:29 CST 2022,发送一条时长:2000ms的消息给ttl队列QC,消息为:你好2
2022-11-27 12:58:42.600 INFO 64000 --- [ntContainer#0-1] c.w.r.consumer.DeadLetterQueueConsumer : 当前时间:Sun Nov 27 12:58:42 CST 2022,收到死信队列的消息:你好1
2022-11-27 12:58:42.600 INFO 64000 --- [ntContainer#0-1] c.w.r.consumer.DeadLetterQueueConsumer : 当前时间:Sun Nov 27 12:58:42 CST 2022,收到死信队列的消息:你好2
RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列中,如果第一个消息的延时时间很长,而第二个消息并不会优先得到执行。
7.5 RabbitMQ插件实现延迟队列
下载插件:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
# 拷贝到rabbitmq的plugins目录下
cp rabbitmq_delayed_message_exchange-3.9.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.9.16/plugins/
#进入plugins目录
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.9.16/plugins/
#安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
#重启服务
systemctl restart rabbitmq-server
@Configuration
public class DelayedQueueConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//routingKey
public static final String DELAYED_ROUTINGKEY = "delayed.routingKey";
//声明交换机
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> arguments = new HashMap<>(3);
arguments.put("x-delayed-type", "direct");
//1:交换机名称 2:类型 3:是否持久化 4:自动删除 5:其它参数
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);
}
//声明队列
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//绑定
@Bean
public Binding delayQBindingDelayExchange(@Qualifier("delayedExchange") CustomExchange delayedExchange
, @Qualifier("delayedQueue") Queue delayedQueue) {
return BindingBuilder
.bind(delayedQueue)
.to(delayedExchange)
.with(DELAYED_ROUTINGKEY)
.noargs();
}
}
/**
* 基于插件的延迟消息
*/
@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(), msg);
}
}
//基于插件
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendDelayMsg(@PathVariable String message, @PathVariable Integer delayTime) {
log.info("当前时间:{},发送一条时长:{}ms的消息给延迟队列delayed.queue,消息为:{}", new Date(), delayTime,
message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,
DelayedQueueConfig.DELAYED_ROUTINGKEY, message, (msg) -> {
//设置消息发送的延迟时长ms
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
达到预期值
2022-11-27 15:15:45.393 INFO 55464 --- [nio-8080-exec-1] c.w.r.controller.SendMsgController : 当前时间:Sun Nov 27 15:15:45 CST 2022,发送一条时长:20000s的消息给延迟队列delayed.queue,消息为:你好1
2022-11-27 15:15:49.401 INFO 55464 --- [nio-8080-exec-8] c.w.r.controller.SendMsgController : 当前时间:Sun Nov 27 15:15:49 CST 2022,发送一条时长:2000s的消息给延迟队列delayed.queue,消息为:你好2
2022-11-27 15:15:51.257 INFO 55464 --- [ntContainer#1-1] c.w.r.consumer.DelayQueueConsumer : 当前时间:Sun Nov 27 15:15:51 CST 2022,收到延迟队列的消息:你好2
2022-11-27 15:16:03.862 INFO 55464 --- [ntContainer#1-1] c.w.r.consumer.DelayQueueConsumer : 当前时间:Sun Nov 27 15:16:03 CST 2022,收到延迟队列的消息:你好1