RabbitMQ的死信队列
死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?
对RabbitMQ来说,产生死信的来源大致有如下几种:
- 消息被拒绝(basic.reject或basic.nack)并且requeue=false.
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加数据到mq中)
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。而死信队列其实就是绑定到死信交换机上的队列
如何配置死信队列
- 正常配置业务队列(绑定到业务交换机上,并配置路由键)
- 配置死信队列(绑定到死信交换机上,并配置路由键),和业务队列是一样的,只是名字不一样
- 将业务队列绑定到死信交换机上,并配置路由键A
死信的处理流程
消息正常发送到业务队列,如果该业务队列拒绝消息,则该消息变成死信,这个时候就会根据路由键A找到对应的死信队列,将消息投放给死信队列,由死信队列处理
修改producer模块
使用到的一些常量
public class Constants {
//直连型交换机
public static final String DEFAULT_DIRECT_EXCHANGE = "defaultDirectExchange";
public static final String DEFAULT_DIRECT_QUEUE = "defaultDirectQueue";
public static final String DEFAULT_DIRECT_ROUTING_KEY = "defaultDirectRoutingKey";
//主题交换机
public static final String DEFAULT_TOPIC_EXCHANGE = "defaultTopicExchange";
public static final String MAN_TOPIC_QUEUE = "manTopicQueue";
public static final String TOTAL_TOPIC_QUEUE = "totalTopicQueue";
public static final String MAN_TOPIC_ROUTING_KEY = "user.man";
public static final String TOTAL_TOPIC_ROUTING_KEY = "user.#";
//扇形交换机
public static final String DEFAULT_FANOUT_EXCHANGE = "defaultFanoutExchange";
public static final String DEFAULT_FANOUT_QUEUE_1 = "defaultFanoutQueue1";
public static final String DEFAULT_FANOUT_QUEUE_2 = "defaultFanoutQueue2";
public static final String DEFAULT_FANOUT_QUEUE_3 = "defaultFanoutQueue3";
}
配置业务队列并绑定到死信队列
@Configuration
public class DirectExchangeConfig {
@Bean
public DirectExchange defaultDirectExchange(){
//直连型交换机,持久化消息,不自动删除
return new DirectExchange(Constants.DEFAULT_DIRECT_EXCHANGE,true,false);
}
@Bean
public Queue defaultDirectQueue(){
//给普通队列绑定死信交换机,设置路由键
Map<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", Constants.DEAD_DIRECT_EXCHANGE);
map.put("x-dead-letter-routing-key", Constants.DEAD_ROUTING_KEY);
//声明队列,持久化消息,非独有,不自动删除,消息属性
return new Queue(Constants.DEFAULT_DIRECT_QUEUE,true,false,false,map);
}
@Bean
public Binding directBiding(){
//将队列和交换机绑定,并设置路由键
return BindingBuilder.bind(defaultDirectQueue()).to(defaultDirectExchange()).with(Constants.DEFAULT_DIRECT_ROUTING_KEY);
}
}
配置死信队列
@Configuration
public class DeadDirectExchangeConfig {
@Bean
public DirectExchange deadDirectExchange(){
return new DirectExchange(Constants.DEAD_DIRECT_EXCHANGE,true,false);
}
@Bean
public Queue deadQueue(){
return new Queue(Constants.DEAD_QUEUE,true,false,false);
}
@Bean
public Binding deadQueueBiding(){
//绑定死信队列和死信交换机、路由key
return BindingBuilder.bind(deadQueue()).to(deadDirectExchange()).with(Constants.DEAD_ROUTING_KEY);
}
}
发送消息
@RequestMapping("/direct/sendMsg")
public void directSendMsg(){
String msg = "direct-msg";
//发送消息到交换机,并携带路由键(会映射到业务队列,业务队列消费失败时,该消息会变成死信,由死信队列消费)
rabbitTemplate.convertAndSend(Constants.DEFAULT_DIRECT_EXCHANGE,Constants.DEFAULT_DIRECT_ROUTING_KEY,msg);
}
修改consumer模块
//监听业务队列
@RabbitListener(queues = Constants.DEFAULT_DIRECT_QUEUE)
public void directConsumer(String msg, Channel channel,Message message) throws IOException {
//死信队列测试使用,直接拒绝消息,将消息变为死信
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}
//监听死信队列
@RabbitListener(queues = Constants.DEAD_QUEUE)
public void deadConsumer(String msg,Channel channel,Message message) throws IOException {
System.out.println("死信队列处理:"+msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
启动两个模块,这个时候要先去控制台删除之前配置过的业务队列defaultDirectQueue,否则会报错,显示已注册的队列找不到x-dead-letter-exchange。
RabbitMQ的延时队列
在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间(过期消息会变为死信)和死信队列来模拟出延时队列。
在 rabbitmq 中存在2种方可设置消息的过期时间,第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。
- 队列设置:在队列申明的时候使用 x-message-ttl 参数(和上面死信队列设置一样),单位为 毫秒
- 单个消息设置:发送消息时设置消息属性的 expiration 参数的值,单位为 毫秒
这里只演示单个消息设置,按消息设置会更灵活一些
producer模块
发送消息时设置过期时间
@RequestMapping("/direct/sendMsg")
public void directSendMsg(){
String msg = "direct-msg";
MessagePostProcessor processor = (message)->{
//设置消息过期时间为3s
message.getMessageProperties().setExpiration("3000");
return message;
};
//发送消息到交换机,并携带路由键
rabbitTemplate.convertAndSend(Constants.DEFAULT_DIRECT_EXCHANGE,Constants.DEFAULT_DIRECT_ROUTING_KEY,msg,processor);
}
consumer模块,将监听业务队列的方法注释掉,这样等3s消息没有被消费,就会被发送到死信队列,由死信队列接收处理
RabbitMQ的延迟插件
RabbitMQ消费延时消息,需要满足两个条件
1、消息TTL到期,也就是说该消息变为死信
2、 该消息在队列首部 ,众所周知队列是先进先出的,只有排在队列首部的消息,才可以被消费,但是很多人都会忽略这一点
来一个实际的应用场景:加入A消息过期时间是一个小时,A消息先进队列,B消息过期时间是10分钟,B消息后进队列,等到十分钟过去了,B消息并没有被死信队列消费,而是会等到一个小时过去,先消费A消息,A消息消费成功,这时B消息才排到队列首部,满足消费的两个条件,接着消费B消息
为了解决这种问题RabbitMQ提供了一个延迟插件 rabbitmq_delayed_message_exchange
下载地址:https://www.rabbitmq.com/community-plugins.html
下载完成之后放到plugins文件夹下,接着进入bin文件夹下,
输入命令: rabbitmq-plugins enable rabbitmq_delayed_message_exchange,启动延迟插件
输入命令:rabbitmq-plugins list 查看插件是否安装成功
如上图,说明插件安装成功
接着修改死信交换机和死信队列配置
@Bean
public Queue deadQueue(){
//死信队列
return new Queue(MqConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME,true,false,false);
}
@Bean
public Binding deadQueueBiding(){
//绑定死信队列和死信交换机、路由key
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(MqConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME).noargs();
}
@Bean
public CustomExchange deadExchange(){
//死信交换机,使用插件rabbitmq_delayed_message_exchange
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-delayed-type", "direct");
return new CustomExchange(MqConstants.DEAD_EXCHANGE, "x-delayed-message",true,false, arguments);
}
启动服务,进入控制台可以看到,死信交换机类型为:x-delayed-type
发送延迟消息
public void sendDelay(String queueName, String msg, Integer times) {
logger.info("延时队列发送:队列名:{},消息:{},延时时间(毫秒):{}",queueName, msg, times);
rabbitTemplate.convertAndSend(MqConstants.DEAD_EXCHANGE,MqConstants.DEFAULT_REPEAT_TRADE_QUEUE_NAME,
JSON.toJSONString(msg), message->{
//设置延迟时间,单位:毫秒
message.getMessageProperties().setDelay(times);
return message;
});
}
消费消息和之前一样,就不写了,亲测可用
项目代码:项目代码
相关阅读:
RabbitMQ入门
SpringBoot整合RabbitMQ
RabbitMQ的消息确认机制,消息重试机制