一.基础知识
-
概念:用来存放需要在指定时间被处理的元素的队列,即希望到了指定时间以前或以后处理队列内的元素
-
关系:属于死信队列三种情况中消息TTL过期的情况(从下图可表现出来)
-
结构效果图
说明:producer设置消息有效时间为t,如果c1这时不能处理消息,消息会在normal-queue中停留t时间,如果t之后c1还是不能处理信息,就会进入dead_exchange,进而通过死信队列给到c2来处理消息。从producer和c2的角度看,消息从发出到处理经过了时间t,就像是被延迟了t时间 -
使用场景
1.订单在十分钟之内未支付则自动取消
2.用户发起退款,如果三天内没有处理则通知相关运营人员… -
相比定时任务
1.定时任务适用于数据量少和时间要求宽松的情况,比如“如果账单一周内未支付则自动结算”
2.延迟队列适用于数据量大和时效性强的情况,比如“订单在十分钟之内未支付则自动取消”
二.整合springboot并测试
- 添加依赖:最重要的是spring-boot-starter-amqp,其他依赖为辅助测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<version>2.3.1</version>
</dependency>
- 修改配置文件(不要完全复制粘贴,数值填自己的)
spring.rabbitmq.host=192.168.121.129
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
-
队列TTL代码架构图
说明:创建两个队列QA和QB,两队列TTL分别设置为10s和40s,然后在创建一个交换机X和死信交换机Y,类型都是direct,创建一个死信队列QD,各部分绑定关系见上图连线 -
创建配置类
@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";
//声明xExchange别名
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明yExchange别名
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明普通队列TTL为10s
@Bean("queueA")
public Queue queueA(){
Map<String,Object> map = new HashMap<>(3);
//设置死信交换机
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//设置死信routingkey
map.put("x-dead-letter-routing-key", "YD");
//设置TTL
map.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(map).build();
}
@Bean("queueB")
public Queue queueB(){
Map<String,Object> map = new HashMap<>(3);
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", "YD");
map.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(map).build();
}
//死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
//绑定
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange exchange){
return BindingBuilder.bind(queueA).to(exchange).with("XA");
}
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange exchange){
return BindingBuilder.bind(queueB).to(exchange).with("XB");
}
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange exchange){
return BindingBuilder.bind(queueD).to(exchange).with("YD");
}
}
- 创建生产者:发送延迟消息
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间为:{},发送一条消息给两个TTL队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("X", "XA","消息来自TTL为10s的队列:" +message);
rabbitTemplate.convertAndSend("X", "XB","消息来自ttl为40s的队列:" + message);
}
- 创建消费者:接收消息
@Component
@Slf4j
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")
public void receive(Message message){
String s = new String(message.getBody());
log.info("当前时间为:{},收到死信队列的信息:{}", new Date().toString(), s);
}
}
三.优化延迟队列
- 原因:每增加一个新的时间需求,就要新增一个队列
- 架构图
说明:新增一个队列QC,不设置TTL时间,而是通过producer发消息的时候指定延迟时间,从而不用通过新增队列来满足新的时间需求 - 配置类优化
//之前的代码基础上新增普通队列QC
public static final String QUEUE_C = "QC";
@Bean("queueC")
public Queue queueC(){
Map<String, Object> map = new HashMap<>(3);
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", "YD");
return QueueBuilder.durable(QUEUE_C).withArguments(map).build();
}
- 生产者优化
//通过地址参数ttlTime设置延迟时间
@GetMapping("/sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间:{},发送一条时长{}毫秒TTL消息给队列QC:{}",
new Date().toString(),ttlTime,message);
//以下为部分convertAndSend参数
//参数1:exchange
//参数2:routingkey
rabbitTemplate.convertAndSend("X", "XC", message,msg->{
//设置延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
- 结果展示
说明死信的问题:发现延迟时间2000比20000短,但最后消息却不是延迟时间短的先收到,原因在于rabbitmq只会检测第一个消息是否过期,如果第一个消息延时太长,那之后的消息即使延时很短也不会优先执行
四.基于插件实现延迟队列
- 原因:解决主题三中由死信队列实现延迟队列出现的问题
1.安装插件
- 资源链接:https://pan.baidu.com/s/1lwHInWuvIwOJiXDmJIye-Q;提取码:ufrp
- 步骤一:进入rabbitmq安装目录下的plugins目录,命令如下:(注意…根据自己的来写,X版本号也是)
/.../rabbitmq/lib/rabbitmq_server-X.X.X/plugins
- 步骤二:执行如下命令使插件生效,然后重启rabbitmq
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 验证添加插件成功如下:出现如下x-delayed-message选项
2.实现结构改变
- 死信队列下的延迟队列架构图
- 基于插件的延迟队列架构图
- 综上可知,延迟功能由队列主导变为了由交换机主导
3.简单测试
-
代码架构示意图
-
创建配置类
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
@Bean
public CustomExchange delayedExchange(){
Map<String,Object> map = new HashMap<>();
map.put("x-delayed-type", "direct");
/**
* 参数1:交换机名
* 参数2:交换机类型
* 参数3:是否持久化
* 参数4:是否自动删除
* 参数5:其他
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message",
true,false,map);
}
@Bean
public Binding delayedBinding(@Qualifier("delayedQueue") Queue delayedQueue,
@Qualifier("delayedExchange") CustomExchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
- 添加生产者
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间:{},发送一条时长{}毫秒TTL消息给延迟队列delayed.queue:{}",
new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message, msg->{
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
- 添加消费者
@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().toString(), msg);
}
}
- 测试结果如下:实现了让多条消息中延迟时间短的先处理,提高效率
当前时间:Sun Nov 07 19:33:32 CST 2021,发送一条时长20000毫秒TTL消息给延迟队列delayed.queue:hello1
当前时间:Sun Nov 07 19:33:45 CST 2021,发送一条时长2000毫秒TTL消息给延迟队列delayed.queue:hello2
当前时间:Sun Nov 07 19:33:47 CST 2021,收到延迟队列的消息:hello2
当前时间:Sun Nov 07 19:33:52 CST 2021,收到延迟队列的消息:hello1