介绍
死信,顾名思义就是无法被消费的消息。一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费;
这样的消息如果没有后续的处理,就变成了死信,接收死信消息的队列就叫死信队列;
流程
- 创建一组死信交换机队列
- 创建一组正常的交换机队列,并在创建队列时,指定死信交换机
- 当触发死信条件是,则消息会转发到死信交换机中
成为死信的条件
一个消息如果满足下列条件之一,会进入到死信路由(注意是路由,不是队列,一个路由可以对应多个队列):
- 消息的TTL到了,消息过期了仍然没有被消费;
- 消息被consumer拒收了(手动确认中调用basicReject或者basicNack),并且拒收方法的requeue参数是false,也就是说不会重新入队被其他消费者消费
- 队列的长度限制满了,排在前面的消息会被丢弃或者进入死信路由
一旦某个队列中有消息满足了成为死信的条件,如果该队列设置了死信交换机(Dead Letter Exchange)和死信路由键,那么满足死信条件的消息就会交由死信交换机,死信交换机会根据死信路由键将死信消息投递到对应的死信队列
注意:死信交换机本质上就是一个普通的交换机,只是因为队列设置了参数指定了死信交换机,这个普通的交换机才成为了死信的接收者
消息的TTL过期
TTL指消息的存活时间,如果消息从进入队列开始,直到达到TTL仍然没有被任何消费者消费,那么这个消息将成为死信;
RabbitMQ可以对队列和消息分别设置TTL。对队列设置TTL对队列中的所有消息都生效。如果队列和消息同时设置了TTL,那么会取TTL小的。可以通过设置消息的expiration字段或者队列的x-message-ttl属性来设置TTL;
我们按照如下架构在rabbitmq中创建队列交换机和队列:
其中的要点是:
- 要给普通队列normal.queue设置以下参数:
- x-message-ttl:指定消息过期TTL;
- x-dead-letter-exchange:指定队列关联的死信交换机;
- x-dead-letter-routing-key:指定队列死信交换机绑定的路由键
- 死信交换机dead.letter.exchange要通过指定的死信路由键x-dead-letter-routing-key绑定到死信队列dead.letter.queue
@Configuration
@Slf4j
public class RabbitConfig {
// 添加json格式序列化器
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
// 创建普通交换机
@Bean
public Exchange normalExchange(){
return ExchangeBuilder.directExchange("normal.exchange").durable(true).build();
}
// 创建普通队列,设置ttl为5秒,绑定死信交换机
@Bean
public Queue normalQueue(){
return QueueBuilder.durable("normal.queue").ttl(5000)
.deadLetterExchange("dead.letter.exchange").deadLetterRoutingKey("dead").build();
}
// 创建普通交换机和普通队列的绑定关系
@Bean
public Binding normalBinding(@Qualifier("normalExchange") Exchange exchange, @Qualifier("normalQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
}
// 创建死信交换机
@Bean
public Exchange deadLetterExchange(){
return ExchangeBuilder.directExchange("dead.letter.exchange").durable(true).build();
}
// 创建死信队列
@Bean
public Queue deadLetterQueue(){
return QueueBuilder.durable("dead.letter.queue").build();
}
// 创建普通交换机和普通队列的绑定关系
@Bean
public Binding deadLetterBinding(@Qualifier("deadLetterExchange") Exchange exchange, @Qualifier("deadLetterQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("dead").noargs();
}
}
测试发送5条消息给普通交换机normal.exchange
@Test
public void testSendMessage() {
for (int i = 0; i < 5; i++) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("normal.exchange", "normal",
new User("baobao" + i, 18, new Date()), correlationData);
}
}
发现一开始消息出现在普通队列:
5秒之后TTL到期,消息会全部转移到死信队列中:
消息被consumer拒收
沿用上一小节的架构,只是在创建普通队列时去掉TTL设置
注意:由于修改了普通队列的设置,所以在后续启动程序之前要先在控制台删掉原来的普通队列由程序重新创建,否则会报错
然后在普通队列消费者中拒收消息(注意拒收的前提是要开启消息的手动确认)
@Component
@Slf4j
public class UserConsumer {
@RabbitListener(queues = "normal.queue")
public void handleUserMessage(Message message, User user, Channel channel) throws IOException {
log.info(user.toString());
// 拒收消息,不重新入队,让消息成为死信
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
此时发送消息之后再启动消费者,消息将会全部转移到死信队列中
队列达到最大长度
继续沿用之前的架构,只是要在创建普通队列时添加x-max-length
参数,指定队列的最大长度
// 创建普通队列
@Bean
public Queue normalQueue(){
return QueueBuilder.durable("normal.queue").maxLength(5) // 指定最大长度为5
.deadLetterExchange("dead.letter.exchange").deadLetterRoutingKey("dead").build();
}
此时尝试发送8条消息给普通队列,最终会发现普通队列只有5条消息,另外3条消息被转移到了死信队列