接着上一篇。
SpringBoot整合RabbitMQ实现延迟队列
了解到在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行,这就是用死信做延迟队列的一个缺陷。
如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。
安装延时队列插件
在官网上下载 https://www.rabbitmq.com/community-plugins.html
版本不能乱下载。
下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
插件安装成功,重启MQ
systemctl restart rabbitmq-server
现在去MQ可视化后台管理添加交换机,发现多了一个选项
代码架构图
在这里新增了一个队列 delayed.queue,一个自定义延迟交换机 delayed.exchange,绑定关系如下:
配置文件类代码
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并
不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才
投递到目标队列中。
package com.xiang.springboot_rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/**
* 延迟队列配置
*
*/
@Configuration
public class DelayedQueueConfig {
//交换机
public static final String DELAYED_EXCHANGE ="delayed_exchange";
//队列
public static final String DELAYED_QUEUE ="delayed_queue";
//routeingKey
public static final String DELAYED_ROUTINGKEY ="delayed_routingKey";
//声明延迟交换机
@Bean
public CustomExchange delayedExchange(){
HashMap<String, Object> arguments = new HashMap<>();
//自定义交换机的类型
arguments.put("x-delayed-type", "direct");
/**
* 交换机名
* 交换机类型
* 持久化
* 自动删除
*/
return new CustomExchange(DELAYED_EXCHANGE,"x-delayed-message",true,false,arguments);
}
/**
* 声明队列
* @return
*/
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE);
}
//延迟交换机和队列绑定
@Bean
public Binding delayedQueueBindingDelayedExchange(Queue delayedQueue,CustomExchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTINGKEY).noargs();
}
}
消息生产者代码
@GetMapping("/delay/{message}/{delayedTime}")
public void delayedTimeMessage(@PathVariable String message,@PathVariable Integer delayedTime){
log.info("当前时间:{} 发送一条信息给delayed交换机{},delayedTime:{}",new Date().toString(),message,delayedTime);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE,DelayedQueueConfig.DELAYED_ROUTINGKEY,message,(msg -> {
//发送消息 并设置delayedTime
msg.getMessageProperties().setDelay(delayedTime);
return msg;
}));
}
消息消费者代码
import com.rabbitmq.client.Channel;
import com.xiang.springboot_rabbitmq.config.DelayedQueueConfig;
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;
/**
* 基于查询的监听延迟队列的消息
*/
@Component
@Slf4j
public class delayedConsumer {
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE)
public void receiveMessage(Message message, Channel channel){
String msg = new String(message.getBody());
log.info("当前时间{},收到延迟队列的消息:{}",new Date().toString(),msg);
}
}
启动进行测试
127.0.0.1:8080/ttl/delay/张三/20000
127.0.0.1:8080/ttl/delay/李四/2000
输出结果
结论
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz
或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景