概念
死信队列,顾名思义就是无法被消费的消息,,某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,加入到私信队列中。
死信的来源
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
代码演示ttl过期效果
模拟下图关系
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
设置配置文件
# 服务端口
spring.rabbitmq.host=192.168.136.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
default-requeue-rejected要改为false,代表消费方拒绝消息后不会重新放回普通队列里。
创建生产者
@Component
public class Producer{
@Autowired
private RabbitTemplate rabbitTemplate;
// 2: 路由key
private String routeKey = "zhangsan";
public void makeOrder() {
// 发送消息给所有客户
for(int i = 1;i<=10;i++){
String content = "消息"+i;
System.out.println("生产者发送消息:"+i)
r abbitTemplate.convertAndSend(RabbitConfig.NORMAL_EXCHANGE,
routeKey, content, message -> {
//设置消息过期时间10s,如果十秒内没有被消费者接受,就自动转入死信队列
message.getMessageProperties().setExpiration("10000");
retrun message;
});
}
}
}
创建配置类
@Configuration
public class RabbitConfig {
//普通交换机名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORMAL_QUEUE = "normal_queue";
//死信死信队列名称
public static final String DEAD_QUEUE = "dead_queue";
//声明队列
@Bean("normalQueueDeclared")
public Queue normalQueueDeclared() {
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
return QueueBuilder.durable(NORMAL_QUEUE ).withArguments(params).build();
}
//声明死信队列
@Bean("deadlQueueDeclared")
public Queue deadlQueueDeclared() {
return QueueBuilder.durable(DEAD_QUEUE).build();
}
//声明普通交换机
@Bean("myNormalExchange")
public DirectExchange myNormalExchange() {
return new DirectExchange(NORMAL_EXCHANGE);
}
//声明死信交换机
@Bean("myDeadExchange")
public DirectExchange myDeadExchange() {
return new DirectExchange(DEAD_EXCHANGE);
}
//绑定将队列和交换机绑定
@Bean
public Binding binding1(@Qualifier("normalQueueDeclared") Queue normalQueueDeclared,
@Qualifier("myNormalExchange") DirectExchange myNormalExchange) {
return BindingBuilder.bind(normalQueueDeclared).to(myNormalExchange).with("zhangsan");
}
@Bean
public Binding binding2(@Qualifier("deadlQueueDeclared") Queue deadlQueueDeclared,
@Qualifier("myDeadExchange") DirectExchange myDeadExchange) {
return BindingBuilder.bind(deadlQueueDeclared).to(myDeadExchange).with("lisi");
}
}
消费方单独建立一个boot工程,只启动生产者不启动消费方,模拟消息过期的效果, c1
// bindings用来确定队列和交换机绑定关系
@RabbitListener(bindings =@QueueBinding(
//指定获取信息的队列
value = @Queue(value = RabbitConfig.NORMAL_QUEUE ,autoDelete = "false"),
// 指定交换机
exchange = @Exchange(value = RabbitConfig.NORMAL_EXCHANGE,
// 这里是确定的交换机的模式
type = ExchangeTypes.DIRECT)
))
@Component
public class Consumer01 {
// @RabbitHandler 代表此方法是一个消息接收的方法。该不要有返回值
@RabbitHandler
public void messagerevice(String message){
// 此处省略发邮件的逻辑
System.out.println("c1-------------->" + message);
}
}
消费方c2
// bindings用来确定队列和交换机绑定关系
@RabbitListener(bindings =@QueueBinding(
//指定获取信息的队列
value = @Queue(value = RabbitConfig.DEAD_QUEUE ,autoDelete = "false"),
// 指定交换机
exchange = @Exchange(value = RabbitConfig.DEAD_EXCHANGE,
// 这里是确定的交换机的模式
type = ExchangeTypes.DIRECT)
))
@Component
public class Consumer02 {
// @RabbitHandler 代表此方法是一个消息接收的方法。该不要有返回值
@RabbitHandler
public void messagerevice(String message){
// 此处省略发邮件的逻辑
System.out.println("c2-------------->" + message);
}
}
观察结果
生产者发送了10条消息,因为消费方没有启动所以显示10条消息未消费
10秒后消息过期,自动加入到死信队列
启动消费方c2,此时就可以看到死信队列中的数据已经被c2消费掉了
c2--------------> 消息1
c2--------------> 消息2
c2--------------> 消息3
c2--------------> 消息4
c2--------------> 消息5
c2--------------> 消息6
c2--------------> 消息7
c2--------------> 消息8
c2--------------> 消息9
c2--------------> 消息10
队列达到最大长度代码演示
生产者取消发送消息的ttl
@Component
public class Producer{
@Autowired
private RabbitTemplate rabbitTemplate;
// 2: 路由key
private String routeKey = "zhangsan";
public void makeOrder() {
// 发送消息给所有客户
for(int i = 1;i<=10;i++){
String content = "消息"+i;
System.out.println("生产者发送消息:"+i)
r abbitTemplate.convertAndSend(RabbitConfig.NORMAL_EXCHANGE,
routeKey, content, null);
}
}
}
设置普通队列的最大长度
//声明队列
@Bean("normalQueueDeclared")
public Queue normalQueueDeclared() {
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
//设置队列的最大长度
params.put("x-max-length", 6);NORMAL_QUEUE
return QueueBuilder.durable(NORMAL_QUEUE).withArguments(params).build();
}
//设置队列的最大长度,一旦超过最大长度以外的消息都会路由到死信队列
params.put(“x-max-length”, 6);
观察结果
- 不启动消费者,否则队列中的消息会被及时消费,很难演示出效果。
- 普通队列我们设置了最大长度6,所以可以看到有4条消息路由到了死信队列。
消费者拒绝消息代码演示
c1设置拒绝消费
// bindings用来确定队列和交换机绑定关系
@RabbitListener(bindings =@QueueBinding(
//指定获取信息的队列
value = @Queue(value = RabbitConfig.NORMAL_QUEUE ,autoDelete = "false"),
// 指定交换机
exchange = @Exchange(value = RabbitConfig.NORMAL_EXCHANGE,
// 这里是确定的交换机的模式
type = ExchangeTypes.DIRECT)
))
@Component
public class Consumer01 {
// @RabbitHandler 代表此方法是一个消息接收的方法。该不要有返回值
@RabbitHandler
public void messagerevice(Channel channel, Message message){
String content = new String(message.getBody())
System.out.println("消费者: " + content );
if("消息1".equals(content )){
//requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
System.out.println("C1 接收到消息" + content + "并拒绝签收该消息");
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false)
}
// 此处省略发邮件的逻辑
System.out.println("c1-------------->" + message);
}
}
启动生产者发消息
启动c1消费消息
因为c1拒绝了一个消息,所以该消息被路由到了死信队列中。
死信队列的应用场景
一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。
总结
死信队列其实并没有什么特别,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。