三、RabbitMQ 的死信队列

216 阅读2分钟

一、什么是死信

  无法被消费的消息就称作死信。在RabbitMQ中,生产者将消息投递到 broker 中或者直接投递到 queue 中,消费者从 queue 中拉取消息进行消费。但是由于某些异常情况下 queue 中的消息一致无法被消费者消费,这样的消息如果没有特殊的处理,它就是死信,有死信就衍生出死信队列。

二、如何产生死信

  • 消息设置过期时间:TTL
  • 消息队列设置了最大长度(队列满了,生产者发送的消息数据就无法添加到队列中去)
  • 消息被拒绝

三、实战死信队列

死信队列过程说明

掘金-死信队列.drawio.png

  • 消息生产者TtlProducer

  • 正常队列交换机exchange.topic.ttl.test

  • 消息队列 normal.queue

  • 正常队列消费者 TtlConsumer


  • 死信队列交互exchange.topic.dead.letter

  • 死信队列dead.queue

  • 死信队列消费者DeadLetterConsumer

1. 消息过期时成为死信

1.1 消息生产者 TtlProducer

public class TtlProducer {
    private static String exchangeName = "exchange.topic.ttl.test";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);
        // 设置消息过期时间为 10 秒
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

        String body = "it's ttl message";
        String normalRoutingKey = "rk.normal.queue";
        for (int i = 1; i <= 10; i++) {
            String msg = i + ".".concat(body);
            channel.basicPublish(exchangeName, normalRoutingKey, properties, msg.getBytes(StandardCharsets.UTF_8));
        }
        System.out.println("TtlProducer done");
    }
}

1.2 正常队列消费者 TtlConsumer

public class TtlConsumer {
    // 普通交换机
    private static String exchangeName = "exchange.topic.ttl.test";
    // 死信交换机
    private static String deadLetterExchange = "exchange.topic.dead.letter";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
        channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);

        String deadQueueName = "dead.queue";
        String deadRoutingKey = "rk.dead.queue";
        // 声明死信队列
        channel.queueDeclare(deadQueueName, false, false, false, null);
        // 死信队列绑定到死信交换机的 routingKey
        channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);

        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机
        params.put("x-dead-letter-exchange", deadLetterExchange);
        //正常队列设置死信 routing-key
        params.put("x-dead-letter-routing-key", deadRoutingKey);

        String normalQueueName = "normal.queue";
        String normalRoutingKey = "rk.normal.queue";
        channel.queueDeclare(normalQueueName, false, false, false, params);
        channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);

        System.out.println("TtlConsumer 等待接收消息......");
        // 模拟消费异常情况
        System.out.println(1 / 0);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println("TtlConsumer 接收到消息:" + new String(body) + ",然后抛出异常!!!");
            }
        };
        channel.basicConsume(normalQueueName, true, consumer);
    }
}

1.3 死信队列消费者 DeadLetterConsumer

public class DeadLetterConsumer {
    private static String deadQueueName = "dead.queue";
    // 死信交换机
    private static String deadLetterExchange = "exchange.topic.dead.letter";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();
        channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);

        String deadRoutingKey = "rk.dead.queue";
        channel.queueDeclare(deadQueueName, false, false, false, null);
        channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);

        System.out.println("DeadLetterConsumer 等待接收消息......");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("DeadLetterConsumer 接受到消息 body : " + new String(body));
            }
        };
        channel.basicConsume(deadQueueName, true, consumer);
    }
}

依次运行 TtlConsumerTtlProducerDeadLetterConsumer,在 RabbitMQ 后台会看到

  • TtlConsumer 启动后,normal.queue 和 dead.queue 中都没有消息 image.png

  • TtlProducer 启动后,normal.queue 中有 10 条消息没有被消费 image.png

  • 10 秒过后看到 dead.queue 中有十条消息 image.png

  • DeadLetterConsumer 启动后消息被正常消费掉

2. 消息数量达到了队列最大长度成为死信

在队列 normal.queue 声明时添加 x-max-length 来限制队列的最大长度,核心代码如下:

2.1 消息生产者 MaxLengthProducer

public class MaxLengthProducer {

    private static String exchangeName = "exchange.topic.ttl.test";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);

        String body = "it's ttl message";
        String normalRoutingKey = "rk.normal.queue";
        for (int i = 1; i <= 10; i++) {
            String msg = i + ".".concat(body);
            channel.basicPublish(exchangeName, normalRoutingKey, null, msg.getBytes(StandardCharsets.UTF_8));
        }

        System.out.println("TtlProducer done");
    }
}

2.2 正常队列消费者 MaxLengthConsumer

public class MaxLengthConsumer {

    // 普通交换机
    private static String exchangeName = "exchange.topic.ttl.test";
    // 死信交换机
    private static String deadLetterExchange = "exchange.topic.dead.letter";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
        channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);

        String deadQueueName = "dead.queue";
        String deadRoutingKey = "rk.dead.queue";
        // 声明死信队列
        channel.queueDeclare(deadQueueName, false, false, false, null);
        // 死信队列绑定到死信交换机的 routingKey
        channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);

        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机
        params.put("x-dead-letter-exchange", deadLetterExchange);
        //正常队列设置死信 routing-key
        params.put("x-dead-letter-routing-key", deadRoutingKey);
        // 设置正常队列的长度限制
        params.put("x-max-length", 6);

        String normalQueueName = "normal.queue";
        String normalRoutingKey = "rk.normal.queue";
        channel.queueDeclare(normalQueueName, false, false, false, params);
        channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);

        System.out.println("TtlConsumer 等待接收消息......");
       
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println("TtlConsumer 接收到消息:" + new String(body));
            }
        };
        channel.basicConsume(normalQueueName, true, consumer);
    }
}

2.3 死信队列消费者 DeadLetterConsumer

依次运行 MaxLengthProducerMaxLengthConsumer  和 DeadLetterConsumer,运行之前到RabbtiMQ后台删除normal.queue队列, 可看到如下运行结果:

  • 启动 MaxLengthProducer

image.png

  • MaxLengthConsumer 消费了6条消息

image.png

  • DeadLetterConsumer 消费了4条消息

image.png

3. 拒绝消息后成为死信

实现该功能的重要API是channel.basicReject(envelope.getDeliveryTag(), false) ,方法的第二个参数 requeue 设置成 false 表示拒绝消息重新入队,该队列如果配置了死信交换机,将会将消息发送到死信队列中

3.1 正常队列消费者 RejectProducer

public class RejectProducer {

    private static String exchangeName = "exchange.topic.ttl.test";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);

        String body = "it's ttl message";
        String normalRoutingKey = "rk.normal.queue";
        for (int i = 1; i <= 10; i++) {
            String msg = i + ".".concat(body);
            channel.basicPublish(exchangeName, normalRoutingKey, null, msg.getBytes(StandardCharsets.UTF_8));
        }

        System.out.println("RejectProducer done");
    }
}

3.2 正常队列消费者RejectConsumer

public class RejectConsumer {

    // 普通交换机
    private static String exchangeName = "exchange.topic.ttl.test";
    // 死信交换机
    private static String deadLetterExchange = "exchange.topic.dead.letter";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitChannelUtil.getChannel();

        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
        channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);

        String deadQueueName = "dead.queue";
        String deadRoutingKey = "rk.dead.queue";
        // 声明死信队列
        channel.queueDeclare(deadQueueName, false, false, false, null);
        // 死信队列绑定到死信交换机的 routingKey
        channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);

        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机
        params.put("x-dead-letter-exchange", deadLetterExchange);
        //正常队列设置死信 routing-key
        params.put("x-dead-letter-routing-key", deadRoutingKey);

        String normalQueueName = "normal.queue";
        String normalRoutingKey = "rk.normal.queue";
        channel.queueDeclare(normalQueueName, false, false, false, params);
        channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);

        System.out.println("RejectConsumer 等待接收消息......");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                if (message.contains("6")) {
                    System.out.println("RejectConsumer basicReject 接收到消息:" + message);
                    // requeue 设置成 false 表示拒绝消息重新入队,该队列如果配置了死信交换机,将会将消息发送到死信队列中
                    channel.basicReject(envelope.getDeliveryTag(), false);
                } else {

                    System.out.println("RejectConsumer basicAck 接收到消息:" + message);
                    channel.basicAck(envelope.getDeliveryTag(), false);

                }
            }
        };
        // 不自动确认,因为我们通过 basicReject 和 basicAck 手动进行了确认
        channel.basicConsume(normalQueueName, false, consumer);
    }
}

3.3 死信队列消费者 DeadLetterConsumer

依次运行 RejectProducerRejectConsumer  和 DeadLetterConsumer,可得如下结果:

  • RejectConsumer 消费了 9 条消息,拒绝了第 6 条消息,而DeadLetterConsumer消费了第 6 条消息

image.png

image.png

四、写在最后

文章代码,rabbitmq-dead-letter