延迟队列,队列内部是有序的,最重要的特性就体现在它的延迟属性上。
延迟队列中的元素是希望在指定时间到了以后或之前取出和处理。简单来说,延迟队列就是用来存放需要在指定时间被处理的元素的队列。
延迟队列属于死信队列的一种,属于消息TTL过期的情况。
依赖:
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.amqp
spring-rabbit-test
test
org.springframework.boot
spring-boot-starter-web
com.alibaba
fastjson
1.1.41
io.springfox
springfox-swagger2
2.8.0
io.springfox
springfox-swagger-ui
2.8.0
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
spring.rabbitmq.host=47.98.241.39
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
package com.example.rabbitmq.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author 且听风吟
* @version 1.0
* @description: swagger配置类
* @date 2022/4/24 0024 13:08
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("boot.spring.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger构建rabbitmq api接口文档")
.description("接口")
.termsOfServiceUrl("https://gitee.com/projects")
.version("1.0")
.build();
}
}
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10s 和 40s,再创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
由于SpringBoot整合了 RabbitMQ,所以队列、交换机等声名只需要在配置文件中配置即可。所以,以上代码架构只需要写三个类:生产者、消费者以及配置文件
其中:
package com.example.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author 且听风吟
* @version 1.0
* @description: TTL队列 具有过期时间的队列配置 配置文件代码
* 声名 一个普通交换机、两个普通队列、一个死信交换机、一个死信队列
* @date 2022/4/24 0024 13:50
*/
@Configuration
public class TtlQueueConfig {
/**
* 普通交换机名称
*/
public static final String EXCHANGE_X = "X";
/**
* 死信交换机名称
*/
public static final String EXCHANGE_DEAD_Y = "Y";
/**
* 普通队列名称
*/
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
/**
* 死信队列名称
*/
public static final String QUEUE_DEAD_D = "QD";
/**
* 声名 普通交换机(EXCHANGE_X)
* @return 普通交换机
*/
@Bean
public DirectExchange EXCHANGEX(){
return new DirectExchange(EXCHANGE_X);
}
/**
* 声名 死信交换机(EXCHANGE_DEAD_Y)
* @return 死信交换机
*/
@Bean
public DirectExchange EXCHANGEDEADY(){
return new DirectExchange(EXCHANGE_DEAD_Y);
}
/**
* 声名普通队列 QUEUE_A TTL为10s
* @return 普通队列 QUEUE_A
*/
@Bean
public Queue QUEUE_A(){
//声名参数
Map<String, Object> arguments = new HashMap<>(3);
//设置死信交换机
arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
//设置死信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();
}
/**
* 声名普通队列 QUEUE_B TTL为40s
* @return 普通队列 QUEUE_B
*/
@Bean
public Queue QUEUE_B(){
//声名参数
Map<String, Object> arguments = new HashMap<>(3);
//设置死信交换机
arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
//设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","YD");
//设置 TTL 40s
arguments.put("x-message-ttl",400000);
return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
}
/**
* 声名死信队列 QUEUE_DEAD_D
* @return 死信队列 QUEUE_DEAD_D
*/
@Bean
public Queue QUEUE_DEAD_D(){
return QueueBuilder.durable(QUEUE_DEAD_D).build();
}
/**
* 普通队列QA 通过 routingKey XA 绑定 普通交换机X
* @param QUEUE_A 普通队列QA
* @param EXCHANGEX 普通交换机X
* @return 绑定
*/
@Bean
public Binding queueABindingX(Queue QUEUE_A,DirectExchange EXCHANGEX){
return BindingBuilder.bind(QUEUE_A).to(EXCHANGEX).with("XA");
}
/**
* 普通队列QB 通过 routingKey XB 绑定 普通交换机X
* @param QUEUE_B 普通队列QB
* @param EXCHANGEX 普通交换机X
* @return 绑定
*/
@Bean
public Binding queueBBindingX(Queue QUEUE_B,DirectExchange EXCHANGEX){
return BindingBuilder.bind(QUEUE_B).to(EXCHANGEX).with("XB");
}
/**
* 死信队列QD 通过 routingKey YD 绑定 死信交换机Y
* @param QUEUE_DEAD_D 死信队列QD
* @param EXCHANGEDEADY 死信交换机Y
* @return 绑定
*/
@Bean
public Binding queueDBindingX(Queue QUEUE_DEAD_D,DirectExchange EXCHANGEDEADY){
return BindingBuilder.bind(QUEUE_DEAD_D).to(EXCHANGEDEADY).with("YD");
}
}
package com.example.rabbitmq.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @author 且听风吟
* @version 1.0
* @description: 发送延迟消息
* @date 2022/4/24 0024 17:58
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送给两个TTL队列:{}",new Date().toString(),message);
//生产者使用普通交换机X 通过 信道XA 向普通队列QA发送消息
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
//生产者使用普通交换机X 通过 信道XB 向普通队列QB发送消息
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
}
}
package com.example.rabbitmq.consumer;
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.util.Date;
/**
* @author 且听风吟
* @version 1.0
* @description: 队列TTL 的消费
* @date 2022/4/25 0025 10:18
*/
@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);
}
}
启动spring boot项目,如果报空指针异常可以降低spring boot版本或者在配置文件中添加:
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
浏览器发起一个请求:http://localhost:8080/ttl/sendMsg/嘿嘿嘿
观察控制台的消息接收状况:
不过,如果这样使用的话,那么每增加一个新的时间需求,就要重新创建一个队列,如果是会议提前通知,岂不是需要创建无数个队列?所以,这里的代码需要优化
在配置文件类中添加如下代码:
/**
* 普通队列的名称
*/
public static final String QUEUE_C = "QC";
/**
* 声名普通队列 QUEUE_C
* @return
*/
@Bean
public Queue QUEUE_C(){
Map<String, Object> arguments = new HashMap<>();
//设置死信交换机
arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
//设置死信routingKey
arguments.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
}
/**
* 普通队列QC 通过 routingKey XC 绑定 普通交换机X
* @param QUEUE_C 普通队列QC
* @param EXCHANGEX 普通交换机X
* @return 绑定
*/
@Bean
public Binding queueCBindingX(Queue QUEUE_C,DirectExchange EXCHANGEX){
return BindingBuilder.bind(QUEUE_C).to(EXCHANGEX).with("XC");
}
在生产者类中添加如下代码:
@GetMapping("/sendTTLMsg/{message}/{ttlTime}")
public void sendMsg(String message,String ttlTime){
log.info("当前时间:{},发送一条时长为{}ms的TTL信息给队列QC:{}",new Date().toString(),ttlTime,message);
//生产者使用普通交换机X 通过 信道XC 向普通队列QC发送消息
rabbitTemplate.convertAndSend("X","XC","消息来自队列QC:"+message,msg ->{
//设置发送消息时的延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
启动项目,发送请求:
http://localhost:8080/ttl/sendTTLMsg/hello1/20000
http://localhost:8080/ttl/sendTTLMsg/hello2/2000
发送了两条消息,第一条消息为hello1,TTL为20s,第二条消息为hello2,TTL为2s。观察到第一条消息先到,而第二条消息后到。
上面提到的问题,如果不能实现在消息力度上的TTL,并使其在设置的TTL时间即时死亡,就无法设计成一个通用的延迟队列。下面来使用插件来解决这个问题
基于插件实现延迟队列的原理图:
由交换机实现队列延迟
在github上下载:Releases · rabbitmq/rabbitmq-delayed-message-exchange (github.com)
放置到 RabbitMQ 插件目录
插件目录:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.28/plugins
在插件目录下执行如下面命令使插件生效:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启RabbitMQ:systemctl restart rabbitmq-server
安装成功之后,在后台管理界面点击新增交换机,选择类型时可以看到新增了 x-delayed-message (延迟消息)类型
在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
在我们自定义的交换机中,x-delayed-message (延迟消息类型)是一种新的交换机类型,该类型消息支持延迟投递机制
配置文件类代码:
package com.example.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import java.util.HashMap;
import java.util.Map;
/**
* @author 且听风吟
* @version 1.0
* @description: 配置延迟消息类型交换机队列
* @date 2022/4/26 0026 10:51
*/
@Configuration
public class DelayedQueueConfig {
/**
* 队列
*/
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
/**
* 延迟交换机
*/
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
/**
* routingKey
*/
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
/**
* 声名队列
* @return 队列
*/
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
/**
* 声名延迟队列交换机 采用direct模式
* CustomExchange:自定义交换机
* @return 延迟队列交换机
*/
@Bean
public CustomExchange delayedExchange(){
Map<String, Object> arguments =new HashMap<>();
arguments.put("x-delayed-type","direct");
/**
* 1.交换机的名称
* 2.交换机的类型
* 3.是否需要持久化
* 4.是否需要自动删除
* 5.其他参数
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",
true,false,arguments);
}
/**
* 绑定
* @param delayedQueue 队列
* @param delayedExchange 延迟交换机
* @return 绑定关系
*/
@Bean
public Binding delayedBinding(Queue delayedQueue, Exchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
配置在SendMsgController中:
//发送基于插件的消息 及 延迟的时间
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间:{},发送一条时长为{}ms的信息给延迟队列delayed.queue:{}",
new Date().toString(),delayTime,message);
//发消息
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_QUEUE_NAME,
DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg ->{
//设置发送消息时的延迟时长
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
package com.example.rabbitmq.consumer;
import com.example.rabbitmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author 且听风吟
* @version 1.0
* @description: 延迟队列消费者
* 基于插件的延迟消息
* @date 2022/4/26 0026 13:41
*/
@Slf4j
@Component
public class DelayQueueConsumer {
//监听消息
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveMsg(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
}
}
发起请求:
http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000
第二个消息被先消费掉了,符合预期