阅读 383

RabbitMQ系列(十三)RabbitMQ的死信队列

概览

注意:业务队列与死信交换机的绑定是在构建业务队列时,通过参数(x-dead-letter-exchange和x-dead-letter-routing-key)的形式进行指定。

死信的概念

“死信--Dead Letter”是RabbitMQ中的一种消息机制。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列,那么“死信”消息将会被丢进死信队列中,如果没有配置,则“死信”消息将会被丢弃。

死信出现的情况

  1. 消息被否定确认(使用 channel.basicNack 或 channel.basicReject),并且requeue属性被设置为false;
  2. 消息在队列的存活时间超过设置的TTL时间;
  3. 消息队列的消息数量已经超过最大队列长度;

如何配置死信队列

  1. 配置“业务队列”并绑定到“业务交换机”上;
  2. 为业务队列配置“死信交换机”和“路由key”;
  3. 为死信交换机配置“死信队列”;

注意:并不是直接声明一个公共的死信队列,然后所有死信消息就自己跑到死信队列里去。而是为每个需要使用死信的业务队列配置一个死信交换机。同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。

示例

  1. 配置信息
@Configuration
public class RabbitMQConfig {
    public static final String BUSINESS_EXCHANGE_NAME = "business.exchange";
    public static final String BUSINESS_QUEUE_A_NAME = "business.queue_a";
    public static final String BUSINESS_QUEUE_B_NAME = "business.queue_b";
    public static final String DEAD_LETTER_EXCHANGE = "deadletter.exchange";
    public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "dl_queue_a.routingkey";
    public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "dl_queue_b.routingkey";
    public static final String DEAD_LETTER_QUEUE_A_NAME = "queue_a";
    public static final String DEAD_LETTER_QUEUE_B_NAME = "queue_b";

    // 声明业务Exchange
    @Bean("businessExchange")
    public FanoutExchange businessExchange(){
        return new FanoutExchange(BUSINESS_EXCHANGE_NAME);
    }

    // 声明死信Exchange
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    // 声明业务队列A
    @Bean("businessQueueA")
    public Queue businessQueueA(){
        Map<String, Object> args = new HashMap<>(2);
        //声明当前队列绑定的“死信交换机”
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信“路由key”
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUE_A_ROUTING_KEY);
        return QueueBuilder.durable(BUSINESS_QUEUE_A_NAME).withArguments(args).build();
    }

    // 声明业务队列B
    @Bean("businessQueueB")
    public Queue businessQueueB(){
        Map<String, Object> args = new HashMap<>(2);
        //声明当前队列绑定的“死信交换机”
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        //声明当前队列的“死信路由key”
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);
        return QueueBuilder.durable(BUSINESS_QUEUE_B_NAME).withArguments(args).build();
    }

    // 声明死信队列A
    @Bean("deadLetterQueueA")
    public Queue deadLetterQueueA(){
        return new Queue(DEAD_LETTER_QUEUE_A_NAME);
    }

    // 声明死信队列B
    @Bean("deadLetterQueueB")
    public Queue deadLetterQueueB(){
        return new Queue(DEAD_LETTER_QUEUE_B_NAME);
    }

    // 声明业务队列A绑定关系
    @Bean
    public Binding businessBindingA(@Qualifier("businessQueueA") Queue queue,
                                    @Qualifier("businessExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    // 声明业务队列B绑定关系
    @Bean
    public Binding businessBindingB(@Qualifier("businessQueueB") Queue queue,
                                    @Qualifier("businessExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    // 声明死信队列A绑定关系
    @Bean
    public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
                                    @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUE_A_ROUTING_KEY);
    }

    // 声明死信队列B绑定关系
    @Bean
    public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
                                      @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUE_B_ROUTING_KEY);
    }
}
复制代码
  1. 业务队列的消费者
@Slf4j
@Component
public class BusinessMessageReceiver {
    // 监听器处理方法(监听业务队列A——BUSINESS_QUEUE_A_NAME)
    @RabbitListener(queues = BUSINESS_QUEUE_A_NAME)
    public void receiveA(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("收到业务消息A:{}", msg);
        boolean ack = true;
        Exception exception = null;
        try {
            // 如果消息中包含“deadletter”, 则抛出异常,对接收到的消息返回“Nack”确认——让消息进入死信队列中
            if (msg.contains("deadletter")){
                throw new RuntimeException("dead letter exception");
            }
        } catch (Exception e){
            ack = false;
            exception = e;
        }
        if (!ack){
            log.error("消息消费发生异常,error msg:{}", exception.getMessage(), exception);
            // 进行非正常的“nack”确认
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        } else {
            // 进行正常的“ack”确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }

    // 监听器处理方法(监听业务队列B——BUSINESS_QUEUE_B_NAME)
    @RabbitListener(queues = BUSINESS_QUEUE_B_NAME)
    public void receiveB(Message message, Channel channel) throws IOException {
        System.out.println("收到业务消息B:" + new String(message.getBody()));
        // 进行正常的“ack”确认
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
复制代码
  1. 死信队列的消费者
@Component
public class DeadLetterMessageReceiver {
    // 监听器处理方法(监听死信队列A——DEAD_LETTER_QUEUE_A_NAME)
    @RabbitListener(queues = DEAD_LETTER_QUEUE_A_NAME)
    public void receiveA(Message message, Channel channel) throws IOException {
        System.out.println("收到死信消息A:" + new String(message.getBody()));
        // 进行正常的“ack”确认
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
    
    // 监听器处理方法(监听死信队列B——DEAD_LETTER_QUEUE_B_NAME)
    @RabbitListener(queues = DEAD_LETTER_QUEUE_B_NAME)
    public void receiveB(Message message, Channel channel) throws IOException {
        System.out.println("收到死信消息B:" + new String(message.getBody()));
        // 进行正常的“ack”确认
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
复制代码
  1. 生产者
@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController {
    @Autowired
    private BusinessMessageSender sender;

    @RequestMapping("sendmsg")
    public void sendMsg(String msg){
        sender.sendMsg(msg);
    }
}

// 消息发送器
@Component
public class BusinessMessageSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsg(String msg){
        rabbitTemplate.convertSendAndReceive(BUSINESS_EXCHANGE_NAME, "", msg);
    }
}
复制代码

死信队列的应用场景

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有:

  1. 由于消息信息本身存在错误导致处理异常;
  2. 处理过程中参数校验异常;
  3. 因网络波动导致的查询异常等等;

通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。