1 什么是死信
死信,其实这是 RabbitMQ 中一种消息类型,和普通的消息在本质上没有什么区别,更多的是一种业务上的划分。如果队列中的消息出现以下情况之一,就会变成死信:
-
如果给消息队列设置了消息的过期时间(
x-message-ttl
),或者发送消息时设置了当前消息的过期时间,当消息在队列中的存活时间大于过期时间时,就会变成死信。 -
如果给消息队列设置了最大容量(
x-max-length
),队列已经满了,后续再进来的消息会溢出,无法被队列接收就会变成死信。 -
消息接收时被拒绝会变成死信,例如调用
channel.basicNack
或channel.basicReject
,并设置requeue
为false
。
2 什么是死信队列
RabbitMQ死信队列俗称,备胎队列。如果不对死信做任何处理,则消息会被直接丢弃。一般死信都是那些在业务上未被正常处理的消息,我们可以考虑用一个队列来接收这些没有被处理的消息,接收死信消息的队列就是死信队列
,它就是一个普通的消息队列,没有什么特殊的,只是我们在业务上赋予了它特殊的职责罢了,后期再根据实际情况处理死信队列中的消息即可。
下面对以上三种死信队列情况分别讲述:
3 环境准备和死信队列创建
1.首先添加依赖
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.创建死信交换机和队列
其实交换机和队列的创建过程没什么特别的,就是个普通的交换机和队列。
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeadLetterRabbitConfig {
@Bean
DirectExchange deadLetterExchange() {
return new DirectExchange("dead.letter.exchange", true, false);
}
// 创建死信队列
@Bean
Queue deadLetterQueue() {
return new Queue("dead.letter.queue", true);
}
// 绑定队列和交换机
@Bean
Binding deadLetterBinding() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with("dead.letter");
}
}
3.统一设置消息的手动确认机制,在下面拒绝消息等操作时需要用到
spring:
rabbitmq:
# rabbitmq连接配置
host: 127.0.0.1
port: 5672
# virtual-host: # 虚拟主机
username: guest
password: guest
# 确认回调机制,此处也可不写
publisher-confirm-type: correlated
publisher-returns: true
# 手动确认
listener:
direct:
acknowledge-mode: manual
simple:
acknowledge-mode: manual
至于消息什么时候被发送到该队列,取决于业务队列的参数设置,常用的参数如下:
x-dead-letter-exchange
:消息成为死信发送给哪个交换机x-dead-letter-routing-key
:死信交换机绑定队列的routingKeyx-message-ttl
:消息过期时间(ms)x-max-length
:队列接收消息的最大数
详细示例在下面创建业务交换机时会讲解
4 消息过期,无人消费
-
首先创建业务所用的交换机、队列,给队列设置好过期时间、死信发送的交换机、绑定的routing_key。这样消息过期后就会自动发送给死信队列。
注意,如果队列已经创建,之后再修改队列的配置参数,则不会生效,需要删除掉队列重新创建
@Configuration public class RabbitConfig { // 创建交换机 @Bean DirectExchange businessExchange() { return new DirectExchange("business.exchange", true, false); } /** * 创建业务队列,设置属性 * x-message-ttl:多少毫秒过期 * x-dead-letter-exchange:消息成为死信发送给哪个交换机 * x-dead-letter-routing-key:成为死信发送消息的routing_key */ @Bean Queue businessQueue1() { HashMap<String, Object> args = new HashMap<>(); //过期时间:10s args.put("x-message-ttl", 10000); // 设置死信交换机 args.put("x-dead-letter-exchange", "dead.letter.exchange"); // 设置死信交换机绑定队列的routingKey args.put("x-dead-letter-routing-key", "dead.letter"); return new Queue("business.queue1", true, false, false, args); } @Bean Binding businessBinding1() { return BindingBuilder.bind(businessQueue1()).to(businessExchange()).with("businessRoute1"); } }
可以看到已经有了死信交换机和有过期时间的队列
-
发送一条消息,不配置消费者消费
@RestController @RequestMapping("/rabbitmq") public class CabbitmqController { @Autowired RabbitTemplate rabbitTemplate; @PostMapping("/sendMessageToDeadByExpire") public AjaxResult sendMessageToDeadByExpire(@RequestBody Map params) { String id = UUID.randomUUID().toString(); String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); params.put("messageId",id); params.put("createTime",createTime); /** * 发给交换机,通过匹配队列和交换机绑定关系值,判断发送给哪个队列 */ rabbitTemplate.convertAndSend("business.exchange","businessRoute1",params); return AjaxResult.success("成功"); } }
发送消息后,业务队列接收到了一条消息
但十秒之后,消息过期,还没被消费,会被发送到死信队列,如下图死信队列已经存在了一条消息
除了给队列设置消息的超时时间,也可以在发送消息时配置,有兴趣的可以自己尝试。
5 消息溢出(超出队列最大容量)
给队列设置最大承载消息的数量,超出数量则不再接收,发送给死信队列
同样创建business.queue.max
业务消息队列,设置队列的大小为10,设置相同的死信队列
@Bean
Queue businessQueue3() {
HashMap<String, Object> args = new HashMap<>();
// 设置消息队列的大小
args.put("x-max-length", 10);
args.put("x-dead-letter-exchange", "dead.letter.exchange");
args.put("x-dead-letter-routing-key", "dead.letter");
return new Queue("business.queue.max", true, false, false, args);
}
@Bean
Binding businessBinding3() {
return BindingBuilder.bind(businessQueue3()).to(businessExchange()).with("business-max");
}
发送消息,并调用超过十次以上
public AjaxResult sendMessageToDeadByMax(@RequestBody Map params) {
String id = UUID.randomUUID().toString();
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
params.put("messageId",id);
params.put("createTime",createTime);
rabbitTemplate.convertAndSend("business.exchange","business.queue.max",params);
return AjaxResult.success("成功");
}
可以看到发送到十次消息都会保留,当发送第十一次时,超过设置的最大个数,消息发送到死信队列。
6 消息被拒绝
Queue businessQueueNack() {
HashMap<String, Object> args = new HashMap<>();
// 设置死信交换机
args.put("x-dead-letter-exchange", "dead.letter.exchange");
// 设置死信交换机绑定队列的routingKey
args.put("x-dead-letter-routing-key", "dead.letter");
return new Queue("business.queue.nack", true, false, false, args);
}
@Bean
Binding businessBindingNack() {
return BindingBuilder.bind(businessQueueNack()).to(businessExchange()).with("business-Nack");
}
发送消息同上面一致即可
public AjaxResult sendMessageToDeadByNack(@RequestBody Map params) {
String id = UUID.randomUUID().toString();
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
params.put("messageId",id);
params.put("createTime",createTime);
rabbitTemplate.convertAndSend("business.exchange","business-nack",params);
return AjaxResult.success("成功");
}
监听消息方法,监听business.queue.nack
队列,用channel.basicNack
拒绝消息,消息就会被发送到死信队列。
@RabbitHandler
@RabbitListener(queues = "business.queue.nack",ackMode = "MANUAL")
public void processQueueTest1(Map param, Message message, Channel channel) throws IOException {
/**
* 第一个参数是消息的唯一ID
* 第二个参数表示是否批量处理
* 第三个参数表示是否将消息重发回该队列
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
System.out.println("business.queue.nack消费者接收到消息:" + param.toString());
System.out.println("message:" + message);
System.out.println("channel:" + channel);
}
发送消息后,可以看到消费者成功接收到消息,但回复拒绝,消息被转发到死信队列
7 小结
关于死信队列的用法就介绍到这里了,还是很简单的。在一些重要的业务场景中,为了防止有些消息由于各种原因未被正常消费而丢失掉,可以考虑使用死信队列来保存这些消息,以方便后期排查问题使用,这样总比后期再去复现错误要简单的多。其实,延时队列也可以结合死信队列来实现,本文消息过期例子就是它的雏形。