死信队列(DLQ)
1.什么是死信队列?
死信队列是一种用于处理无法被正常消费的消息机制,当消息在队列中变成 '死信' 时,RabbitMq会自动将这些消息重新发送到另一个队列,即死信队列。
那么在消息队列中,什么情况下会变成 '死信'?
(1)消息被拒:消费者使用basic.reject或basic.nack并且将requeue参数设置为false,表示消息不会在次入队
(2)消息过期:为消息设置过期时间(TTL),消息在队列中等待时间比TTL时间更长,则会被认为是死信。(如果整个队列过期,则队列中的消息不是死信消息)
(3)队列达到最大长度:如果队列已满并设置x-max-length属性,则最早进入队列的消息被认为是死信
(4)如果消息被返回到仲裁队列的次数超过了delivery-limit所设定的值,那么这个消息被视为死信(delivery-limit它是参数配置,消息被视为死信之前可以重新投递到队列的最大次数)
注意:上图官网说的quorum queue它是仲裁队列,queue的一种类型,通过Raft算法实现高可用和强一致性,本节先不考虑,感兴趣的话可以去了解下,在未来仲裁队列有可能会取代经典队列。
这些‘死信’的流转过程如下
在RabbitMQ中死信交换机和死信队列就是普通的交换机和队列
2.应用场景
(1)消息重试机制:当消息消费者因为某些临时问题(如数据库连接失败、网络抖动等)而无法正常处理消息时,可以将这些消息放入死信队列,并在稍后重试处理。
(2)消息延迟处理:需要延迟发送的通知系统。当消息到达时,你可以将其发送到一个带有TTL的队列中。如果消息在TTL到期前没有被消费,它就会被发送到死信队列。然后,你可以设置一个消费者来从死信队列中读取消息并发送通知。
3.代码实现
(具体SpringBoot工程请参考SpringBoot集成RabbitMq)
准备正常队列、死信队列及交换机
@Configuration
public class MqConfig {
//正常
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String NORMAL_QUEUE = "normal_queue";
public static final String NORMAL_ROUTE = "normal_route";
//死信
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String DEAD_QUEUE = "dead_queue";
public static final String DEAD_ROUTE = "dead_route";
//死信交换机
@Bean
public Exchange deadExchange() {
return ExchangeBuilder.directExchange(DEAD_EXCHANGE).build();
}
//死信队列
@Bean
public Queue deadQueue() {
return QueueBuilder.durable(DEAD_QUEUE).build();
}
//死信交换机绑定死信队列
@Bean
public Binding deadBinding(Exchange deadExchange,Queue deadQueue) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTE).noargs();
}
//正常队列交换机
@Bean
public Exchange normalExchange() {
return ExchangeBuilder.directExchange(NORMAL_EXCHANGE).build();
}
//正常交换机绑定正常队列
@Bean
public Binding normalBinding(Queue normalQueue,Exchange normalExchange) {
return BindingBuilder.bind(normalQueue).to(normalExchange).with(NORMAL_ROUTE).noargs();
}
//绑定死信交换机及路由key(该正常队列内的消息无法被正常消费时,会转发给绑定的死信交换机通过路由key转发到死信队列)
@Bean
public Queue normalQueue() {
return QueueBuilder.durable(NORMAL_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTE)
.build();
}
}
生产者代码
@RestController
@RequestMapping("/product")
@Slf4j
public class PushMessageController {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MqProductCallBack mqProductCallBack;
@GetMapping("/pushMessage")
public String pushMessage() {
//创建CorrelationData对象,包含唯一id,id的作用是在回调函数中识别消息,也就是根据id跟踪这条消息
CorrelationData correlationData = new CorrelationData("id_" + System.currentTimeMillis());
//消息确认与返回
rabbitTemplate.setConfirmCallback(mqProductCallBack);
rabbitTemplate.setReturnsCallback(mqProductCallBack);
//消息发送
rabbitTemplate.convertAndSend(MqConfig.NORMAL_EXCHANGE, MqConfig.NORMAL_ROUTE, "小桥流水哗啦啦!",
//Lambda表达式,实现MessagePostProcessor接口
message -> {
//获取消息的属性,设置传输模式DeliveryMode为持久化,会写入磁盘
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
//返回修改后的消息
return message;
}, correlationData);
return "消息发送成功了!!!";
}
}
消费者监听死信队列
@Component
@Slf4j
public class DeadLetterListener {
@RabbitListener(queues = "dead_queue")
public void listenerDeadLetter(String msg, Message message, Channel channel) throws IOException {
log.info("--死信队列--接收到的消息是:{}",msg);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
channel.basicAck(deliveryTag,false);
}
}
(1)监听正常队列(消费者实现拒绝消息、否认消息)
@Component
@Slf4j
public class NormalListener {
@RabbitListener(queues = "normal_queue")
/**
* @Header(AmqpHeaders.DELIVERY_TAG)Long tag
* 抽取该参数到方法中,其中AmqpHeaders.DELIVERY_TAG是为消费者提供唯一的标识符;
* 消费者通过DELIVERY_TAG接收或者拒绝消息
* (也可以接收Message取参)
*/
public void handleMessage1(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG)Long tag) throws IOException {
log.info("正常队列接收到的消息是:{}",message);
//拒绝消息
channel.basicReject(tag,false);
//否认消息
// channel.basicNack(tag,false,false);
}
}
控制台输出
监听死信队列的消费者把消息消费下来
(2)超过消息过期时间TTL
生产者代码:
@GetMapping("/pushMessage")
public String pushMessage() {
//创建CorrelationData对象,包含唯一id,id的作用是在回调函数中识别消息,也就是根据id跟踪这条消息
CorrelationData correlationData = new CorrelationData("id_" + System.currentTimeMillis());
//消息确认与返回
rabbitTemplate.setConfirmCallback(mqProductCallBack);
rabbitTemplate.setReturnsCallback(mqProductCallBack);
//消息发送
rabbitTemplate.convertAndSend(MqConfig.NORMAL_EXCHANGE, MqConfig.NORMAL_ROUTE, "小桥流水哗啦啦!",
//Lambda表达式,实现MessagePostProcessor接口
message -> {
//获取消息的属性,设置传输模式DeliveryMode为持久化,会写入磁盘
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
//消息过期时间 5秒
message.getMessageProperties().setExpiration("5000");
//返回修改后的消息
return message;
}, correlationData);
return "消息发送成功了!!!";
}
延迟五秒我们在启动消费者,消费者控制台:监听死信队列消费者把消息消费下来,而监听正常队列没有打印日志,说明没有消息进行消费,也就是5秒后没有消费者去消费这条消息,直接放到死信队列。
(3)队列达到最大长度
队列配置
@Bean
public Queue normalQueue() {
return QueueBuilder.durable(NORMAL_QUEUE)
//队列更改需要删除队列,否则报错
.maxLength(1)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey(DEAD_ROUTE)
.build();
}
消费者控制台
超出长度的变为死信队列被监听死信的消费者消费下来。
代码比较简单,主要是学习死信的流转及如何成为死信,明白了这些你才能去应用,上手更快,面对自己的业务需求才能更好的去拓展。
第一次学习死信队列,希望各位大佬指出不足、不正确的地方!
bay!!!