Bootstrap

【RabbitMQ】- 死信队列

第六章 死信队列

6.1. 死信的概念

​ 先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

​ 应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。

6.2. 死信的来源

  1. 消息 TTL 过期
  2. 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  3. 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false

6.3. 死信实战

6.3.1. 代码架构图

在这里插入图片描述

6.3.2. 消息 TTL 过期

消费者C1代码(比较难写,因为有2个交换机,2个队列)

public class Consumer01 {

    // 普通交换机名称
    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";


    public static void main(String[] args) throws Exception {
        // 获取信道
        Channel channel = RabbitMqUtils.getChannel();

        // 声明普通和死信交换机的类型为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明普通队列
        /**
         * 之前一直将arguments设置为null,这次终于讲到了
         * arguments参数的类型为:Map<String, Object> arguments
         * 而map中可以存放很多参数
         */
        Map<String, Object> arguments = new HashMap<>();
        // 正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 固定写法
        // 设置死信routingKey
        arguments.put("x-dead-letter-routing-key","lisi");
        // 设置过期时间 10s = 10000ms (注意:设置过期时间不仅在这里可以设置,还可以在生产者发送消息的时候设置)
//        arguments.put("x-message-ttl",10000);
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);

        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定普通的交换机与普通的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");

        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
        System.out.println("C1等待接收消息...");


        // 回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println(new String("Consumer01接收的消息是:" + message.getBody(), "UTF-8"));
        };

        // 开启手动应答
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback, consumerTag -> {});
    }
}

生产者代码

public class Producer {

    // 普通交换机名称
    public static final String NORMAL_EXCHANGE = "normal_exchange";

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

        // 发送死信消息(延迟消息),设置TTL时间
        /**
         * 这里通过发布者设置过期时间,通过BasicProperties props参数实现
         *
         */
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties, message.getBytes("UTF-8"));
        }
    }
}

演示:

首先启动Consumer01,使队列中产生相应交换机和队列,然后关闭

然后再启动生产者Producer

可以看到首先会在普通队列中缓存10条消息
在这里插入图片描述
过段时间后,10条消息转移到了死信队列中(因为关闭了消费者1,没人去接收这10条消息)
在这里插入图片描述
再写出消费者C2去处理死信队列中的消息

public class Consumer02 {
    // 死信队列名称
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception {
        // 获取信道
        Channel channel = RabbitMqUtils.getChannel();

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

        // 回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer02接收的消息是:" + new String(message.getBody(), "UTF-8"));
        };

        channel.basicConsume(DEAD_QUEUE,true,deliverCallback, consumerTag -> {});
    }
}

运行C2
在这里插入图片描述
也可以看到死信队列中缓存的消息也消费掉了
在这里插入图片描述

6.3.3. 队列达到最大长度

修改代码:首先将生产者中设置的过期时间注释掉
在这里插入图片描述

public class Producer {

    // 普通交换机名称
    public static final String NORMAL_EXCHANGE = "normal_exchange";

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

        // 发送死信消息(延迟消息),设置TTL时间
        /**
         * 这里通过发布者设置过期时间,通过BasicProperties props参数实现
         *
         */
//        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null, message.getBytes("UTF-8"));
        }
    }
}

在Consumer01中设置队列最大长度(我们设置的是6,共发送10条消息,说明最后会有4条消息进入死信队列)
在这里插入图片描述

public class Consumer01 {

    // 普通交换机名称
    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";


    public static void main(String[] args) throws Exception {
        // 获取信道
        Channel channel = RabbitMqUtils.getChannel();

        // 声明普通和死信交换机的类型为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明普通队列
        /**
         * 之前一直将arguments设置为null,这次终于讲到了
         * arguments参数的类型为:Map<String, Object> arguments
         * 而map中可以存放很多参数
         */
        Map<String, Object> arguments = new HashMap<>();
        // 正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 固定写法
        // 设置死信routingKey
        arguments.put("x-dead-letter-routing-key","lisi");
        // 设置过期时间 10s = 10000ms (注意:设置过期时间不仅在这里可以设置,还可以在生产者发送消息的时候设置)
//        arguments.put("x-message-ttl",10000);
        // 设置正常队列的最大长度
        arguments.put("x-max-length",6);
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);

        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定普通的交换机与普通的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");

        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
        System.out.println("C1等待接收消息...");


        // 回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
           System.out.println(new String("Consumer01接收的消息是:" + message.getBody(), "UTF-8"));
        };

        // 开启手动应答
        channel.basicConsume(NORMAL_QUEUE,false,deliverCallback, consumerTag -> {});
    }
}

测试:
注意:因为我们重新修改过普通队列,如果重启消费者C1代码会报错,所以我们要先删除掉普通队列
在这里插入图片描述

在这里插入图片描述

先启动C1代码,产生普通队列
为了不让者10条消息被消费,我们还是启动C1后再关闭C1,让C1假死,让10条消息都积压在普通队列中
开启生产者代码,可以看到
在这里插入图片描述
再启动C2代码
在这里插入图片描述
在这里插入图片描述

6.3.4. 消息被拒

先修改代码,将C1中的最大长度注释掉
在这里插入图片描述
修改回调函数,拒绝info5

并开启手动应答
在这里插入图片描述

public class Consumer01 {

    // 普通交换机名称
    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";


    public static void main(String[] args) throws Exception {
        // 获取信道
        Channel channel = RabbitMqUtils.getChannel();

        // 声明普通和死信交换机的类型为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明普通队列
        /**
         * 之前一直将arguments设置为null,这次终于讲到了
         * arguments参数的类型为:Map<String, Object> arguments
         * 而map中可以存放很多参数
         */
        Map<String, Object> arguments = new HashMap<>();
        // 正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 固定写法
        // 设置死信routingKey
        arguments.put("x-dead-letter-routing-key","lisi");
        // 设置过期时间 10s = 10000ms (注意:设置过期时间不仅在这里可以设置,还可以在生产者发送消息的时候设置)
//        arguments.put("x-message-ttl",10000);
        // 设置正常队列的最大长度
//        arguments.put("x-max-length",6);
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);

        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定普通的交换机与普通的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");

        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
        System.out.println("C1等待接收消息...");


        // 回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String msg = new String(message.getBody(), "UTF-8");
            if(msg.equals("info5")){
                System.out.println("Consumer01接收的消息是:" + msg + ":此消息是被C1拒绝的");
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
            }else {
                System.out.println("Consumer01接收的消息是:" + msg);
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            }
        };

        // 开启手动应答
        channel.basicConsume(NORMAL_QUEUE,false,deliverCallback, consumerTag -> {});
    }
}

同样的,要先去RabbitMQ删除掉普通队列

演示

先开启C1(此次不用关闭),再开启生产者
在这里插入图片描述
然后再开启C2
在这里插入图片描述
在这里插入图片描述

;