延迟队列
1、概念
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
2、使用场景
1、订单在十分钟之内未支付则自动取消
2、新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3、用户注册成功后,如果三天内没有登陆则进行短信提醒。
4、用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5、预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如: 发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
3、RabbitMQ中 TTL概述
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL 和消息的TTL,那么较小的那个值将会被使用。
有两种方式设置 TTL。
java中消息设置TTL
// 正式发送给交换机,指定routingKey
rabbitTemplate.convertAndSend(TTLDelayQueueConfig.NORMAL_EXCHANGE,
"3",message, (msg)->{
msg.getMessageProperties().setExpiration(String.valueOf(time*1000));
return msg;
});
java中队列设置TTL
@Bean
public Queue delayQueue2(){
HashMap<String, Object> params = new HashMap<>(3);
params.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
params.put("x-dead-letter-routing-key","deadLetter");
// 设置队列中全部消息延迟(存活)时间
params.put("x-message-ttl",5*1000);
return QueueBuilder.durable(DELAY_QUEUE2).withArguments(params).build();
}
4、springBoot使用RabbitMQ 延迟队列
情况如下,java中使用延迟队列,处理延迟消息
代码如下:
1、引入依赖
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- RabbitMQ 测试 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2、配置rabbitMQ
spring:
rabbitmq:
host: xxxxx
port: 5672
username: xxxx
password: xxx
3、配置交换机,和队列,并绑定对应关系
/**
* 配置TTL 延迟队列
*/
@Configuration
public class TTLDelayQueueConfig {
/**
* 两个交换机,一个普通交换机,一个死信交换机
* 三个队列,两个延迟队列(没有人处理消息,消息过期),一个死信队列
*/
public static final String NORMAL_EXCHANGE = "normalExchange";
public static final String DEAD_LETTER_EXCHANGE = "deadLetterExchange";
public static final String DEAD_LETTER_QUEUE = "deadLetterQueue";
public static final String DELAY_QUEUE1 = "delayQueue1";
public static final String DELAY_QUEUE2 = "delayQueue2";
/**
* 声明并创建交换机
* @return
*/
@Bean
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXCHANGE);
}
@Bean
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
/**
* 声明并创建队列
*/
// 延迟队列1 队列中消息的延迟时间为30s
@Bean
public Queue delayQueue1(){
// 将延迟队列绑定死信交换机,指定routingKey 死信消息会发送到对应的queue
HashMap<String, Object> params = new HashMap<>(3);
params.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
params.put("x-dead-letter-routing-key","deadLetter");
// 设置队列中全部消息延迟(存活)时间
params.put("x-message-ttl",30*1000);
return QueueBuilder.durable(DELAY_QUEUE1).withArguments(params).build();
}
// 通过指定routingKey 将队列和对应交换机绑定关系
@Bean
public Binding queue1BindingNormalExchange(Queue delayQueue1,DirectExchange normalExchange){
return BindingBuilder.bind(delayQueue1).to(normalExchange).with("1");
}
// 延迟队列1 队列中消息的延迟时间5s
@Bean
public Queue delayQueue2(){
HashMap<String, Object> params = new HashMap<>(3);
params.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
params.put("x-dead-letter-routing-key","deadLetter");
params.put("x-message-ttl",5*1000);
return QueueBuilder.durable(DELAY_QUEUE2).withArguments(params).build();
}
@Bean
public Binding queue2BindingNormalExchange(Queue delayQueue2,DirectExchange normalExchange){
return BindingBuilder.bind(delayQueue2).to(normalExchange).with("2");
}
// 死信队列
@Bean
public Queue deadLetterQueue(){
return new Queue(DEAD_LETTER_QUEUE);
}
@Bean
public Binding deadQueueBindingDeadExchange(Queue deadLetterQueue,DirectExchange deadLetterExchange){
return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with("deadLetter");
}
}
4、创建死信队列的消费者
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//启动RabbitMQ监听器,监听指定队列是否有消息,有则消费
@RabbitListener(queues = {"deadLetterQueue"})
public void receiveMessage(Message message, Channel channel){
SimpleDateFormat sb = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("当前时间:{},收到死信队列信息 {}", sb.format(new Date()),new String(message.getBody()));
}
}
5、创建生产者,发送消息
@Slf4j
@RestController
@RequestMapping("/delayQueue")
public class SendMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/{message}")
public void sendMessage(@PathVariable String message){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("当前时间: {} , 发送消息给延迟队列(交换机):{}",dateFormat.format(new Date()),message);
// 正式发送给交换机,指定routingKey
rabbitTemplate.convertAndSend(TTLDelayQueueConfig.NORMAL_EXCHANGE,"1",message);
rabbitTemplate.convertAndSend(TTLDelayQueueConfig.NORMAL_EXCHANGE,"2",message);
}
}
测试成功,消息延迟被消费
5、图解延迟队列
6、优化延迟队列
上述过程中,通过队列设置队列中消息的过期时间。如果想要灵活控制消息的过期时间,设置队列过期时间显然不可取。所有要通过生产者控制消息的过期时间。
新增一个队列
/**
* 延迟队列优化
* @return
*/
// 延迟队列3
@Bean
public Queue delayQueue3(){
HashMap<String, Object> params = new HashMap<>(2);
params.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
params.put("x-dead-letter-routing-key","deadLetter");
return QueueBuilder.durable("delayQueue3").withArguments(params).build();
}
@Bean
public Binding queue3BindingNormalExchange(Queue delayQueue3,DirectExchange normalExchange){
return BindingBuilder.bind(delayQueue3).to(normalExchange).with("3");
}
增加一个消费接口
@GetMapping("/{message}/{time}")
public void sendMessage(@PathVariable String message,@PathVariable Long time){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("当前时间: {} , 发送消息给延迟队列(交换机):{}",dateFormat.format(new Date()),message);
// 正式发送给交换机,指定routingKey
rabbitTemplate.convertAndSend(TTLDelayQueueConfig.NORMAL_EXCHANGE,
"3",message, (msg)->{
msg.getMessageProperties().setExpiration(String.valueOf(time*1000));
return msg;
});
}
测试成功
问题:如果发送两天消息给队列,发现处理结果并不符合预期,发送了20s,和10s后过期的消息。消息却是20s后被处理。10s 后应该处理的消息,也是20s后处理。
原因:10s过期的消息,并没有被优先处理,rabbitMQ一直检查队列的第一个消息是否过期,而不会检查之后的消息
7、插件优化延迟队列
1、安装插件
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
2、springboot中使用 插件的延迟队列
结构如下
代码如下
交换机和队列
/**
* 插件配置延迟队列
*/
@Configuration
public class PluginDelayQueueConfig {
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";
/**
* 配置延迟队列
* @return
*/
@Bean
public Queue delayQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
/**
* 自定义延迟交换机
* 类型为: x-delayed-message
*/
@Bean
public CustomExchange delayedExchange(){
HashMap<String, Object> params = new HashMap<>();
//自定义交换机的类型
params.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,params);
}
@Bean
public Binding queueBindingDelayedExchange(Queue delayQueue,CustomExchange delayedExchange){
return BindingBuilder.bind(delayQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
消费者
@Slf4j
@Component
public class DelayedQueueConsumer {
@RabbitListener(queues = {"delayed.queue"})
public void receiveMessage(Message message, Channel channel){
SimpleDateFormat sb = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("当前时间:{} ,接收到消息: {}",sb.format(new Date()),new String(message.getBody()));
}
}
生产者
@GetMapping("/delayExchange/{message}/{time}")
public void sendDelayedMessage(@PathVariable String message,@PathVariable Integer time){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("当前时间: {} , 发送消息给延迟交换机:{}",dateFormat.format(new Date()),message);
// 正式发送消息给延迟交换机,指定routingKey
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY,message, (correlationData)->{
correlationData.getMessageProperties().setDelay(time*1000);
return correlationData;
});
}
运行以后,发现延迟交换机出现了
测试结果, 先发30s过期的消息,后发10s过期的消息。10s消息以过期,就会被处理,不需要等到第一个消息过期后处理。