• 6 RabbitMQ之死信队列



    死信就是无法被消费的消息成为死信。正常情况下,生产者生产的消息投递到交换机,交换机根据routingKey路由到队列中,消费者从队列中消费消息。但由于某些特定原因导致队列中消息无法被消费者正常消费后,消息就变成了死信消息,存放死信消息的队列就成为死信队列。
    死信队列目的就是把未能正常消费的消息保存到死信队列中,保证消息不丢失。

    造成消息死信的原因主要有以下3点:

    • 消息TTL过期,TTL指Time To Live,指消息在队列中存活的时间;
    • 队列达到最大长度,队列已经满,无法再继续添加消息;
    • 消息被拒接,消息被消费者消费后,消费者发送了basic.reject拒绝指令或者basic.nack否定指令。

    下面通过代码演示三种原因下死信队列的处理,如下图所示,首先生产者生产消息投递到normal_exchange交换机,然后路由到normal_queue中,队列中消息可以被Consumer1正常消费的消息会从队列中删除掉,但由于上述三种原因导致消息不能正常被消费的消息就会被重新投递到dead_exchange死信交换机,然后被路由到dead_queue死信队列中,最后死信消息被Consumer2处理。
    在这里插入图片描述

    1. 案例一:消息TTL过期

    首先创建一个Consumer1消费者类,Consumer1类中创建了一个normal_exchange正常交换机,一个dead_exchange死信交换机,创建了一个normal_queue正常队列,一个dead_queue死信队列,并且正常交换机通过routingKey=normal绑定了正常交换机,通过死信交换机通过routingKey=dead绑定了死信队列。然后声明normal_queue队列时还关联了私信交换机,当消息消费失败后消息会重新投递到死信交换机,然后被路由到死信队列中,最后被Consumer2消费。normal_queue队列中可正常消费的消息会被Consumer1进行消费。

    public class Consumer1 {
        private static final String NORMAL_EXCHANGE = "normal_exchange";      /*正常交换机*/
        private static final String DEAD_EXCHANGE = "dead_exchange";        /*死信交换机*/
        private static final String NORMAL_QUEUE = "normal_queue";          /*正常队列*/
        private static final String DEAD_QUEUE = "dead_queue";              /*死信队列*/
        public static void main(String[] args) throws IOException, TimeoutException {
            /*获取信道*/
            Channel channel = RabbitmqUtil.getChannel();
            /*声明死信交换机和普通交换机*/
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
    
            /*声明死信队列*/
            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
            /*死信队列绑定死信交换机*/
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "dead");
    
            /*声明正常队列, 在声明队列前设置队列的属性*/
            Map<String, Object> parms = new HashMap<>();
            /*设置消息过期时间10s,超过10s后消息就会被投递到死信交换机,此参数可以在生产端设置*/
            //parms.put("x-message-ttl", 10000);
            /*正常队列关联死信交换机, 关联key是固定的, 关联后正常队列中消息无法被消费者消费后就会把消息投递到死信交换机*/
            parms.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            /*为正常队列设置消息失败后投递到死信交换机绑定的routingKey, 这样消息消费后就会被死信交换机通过"dead"这个routingKey路由到死信队列中*/
            parms.put("x-dead-letter-routing-key", "dead");
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, parms);
            /*正常队列绑定正常交换机*/
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "normal");
    
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(message);
            };
    
            CancelCallback cancelCallback = consumerTag -> {
                System.out.println("消费失败");
            };
    
            channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, cancelCallback);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    下面创建Consumer2类用于消费死信消息

    import com.lzj.rabbitmq.RabbitmqUtil;
    import com.rabbitmq.client.CancelCallback;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.DeliverCallback;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    public class Consumer2 {
        private static final String DEAD_QUEUE = "dead_queue";              /*死信队列*/
    
        public static void main(String[] args) throws IOException, TimeoutException {
            Channel channel = RabbitmqUtil.getChannel();
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(message);
            };
    
            CancelCallback cancelCallback = consumerTag -> {
                System.out.println("消费失败");
            };
    
            channel.basicConsume(DEAD_QUEUE, true, deliverCallback, cancelCallback);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    最后创建Producer生产者用于生产消息,并对每个消息都设置了在队列中的过期时间10s。

    public class Producer {
        private static final String NORMAL_CHANGE = "normal_exchange";
        public static void main(String[] args) throws IOException, TimeoutException {
            /*获取信道*/
            Channel channel = RabbitmqUtil.getChannel();
            /*设置消息在队列中存活时间,10s后消息过期*/
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
            /*发布消息*/
            String message = null;
            for (int i=1; i<5; i++){
                message = "commodity" + i;
                channel.basicPublish(NORMAL_CHANGE, "normal", properties,  message.getBytes());
                System.out.println("生产者发布消息:" + message);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    下面进行测试,首先运行Consumer1进行正常交换机、死信交换机和正常队列、死信队列的已经绑定关系的创建,运行完Consumer1后从RabbitMQ浏览器管理端可以查看交换机以及队列创建情况,其中normal_queue队列中含有两个属性DLK和DLX,DLX表示死信交换机,DLK表示死信交换机的routingKey。
    在这里插入图片描述
    下面启动生产者Producer,为模拟生产者生产的消息经过10s后都没有被Consumer1消费而进入死信队列,在启动生产者之前先关闭Consumer1,这样生产者生产的消息无法被Consumer1消费从而进入死信队列。运行Producer后,输出如下4个消息,表示生产者生产了下面4个消息

    生产者发布消息:commodity1
    生产者发布消息:commodity2
    生产者发布消息:commodity3
    生产者发布消息:commodity4
    
    • 1
    • 2
    • 3
    • 4

    从浏览器管理端可以看到normal_queue队列中也正好有4条待处理的消息
    在这里插入图片描述
    经过10s后再看浏览器管理端,可以发现normal_queue队列中的4条消息都进到了dead_queue死信队列中
    在这里插入图片描述
    下面启动Consumer2消费者消费掉死信队列中消息,输出下面4条消息,从而验证此4条消息就是生产者生产的4条消息,此4条消息过期后进入的死信队列中。

    commodity1
    commodity2
    commodity3
    commodity4
    
    • 1
    • 2
    • 3
    • 4

    2. 案例二:队列达到最大长度

    设定normal_queue队列最大长度为4,当队列中消息条数达到4条时进入死信队列。
    首先从浏览器管理端删除掉案例一中创建的normal_queue队列,重新创建normal_queue队列,在创建队列过程中通过x-max-length参数指定队列的最大长度

    parms.put("x-max-length", 4);
    
    • 1

    修改后的Consumer1程序如下所示

    public class Consumer1 {
            private static final String NORMAL_EXCHANGE = "normal_exchange";      /*正常交换机*/
            private static final String DEAD_EXCHANGE = "dead_exchange";        /*死信交换机*/
            private static final String NORMAL_QUEUE = "normal_queue";          /*正常队列*/
            private static final String DEAD_QUEUE = "dead_queue";              /*死信队列*/
            public static void main(String[] args) throws IOException, TimeoutException {
                /*获取信道*/
                Channel channel = RabbitmqUtil.getChannel();
                /*声明死信交换机和普通交换机*/
                channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
                channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        
                /*声明死信队列*/
                channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
                /*死信队列绑定死信交换机*/
                channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "dead");
        
                /*声明正常队列, 在声明队列前设置队列的属性*/
                Map<String, Object> parms = new HashMap<>();
                /*设置消息过期时间10s,超过10s后消息就会被投递到死信交换机*/
                //parms.put("x-message-ttl", 10000);
                /*正常队列关联死信交换机, 关联key是固定的, 关联后正常队列中消息无法被消费者消费后就会把消息投递到死信交换机*/
                parms.put("x-dead-letter-exchange", DEAD_EXCHANGE);
                /*为正常队列设置消息失败后投递到死信交换机绑定的routingKey, 这样消息消费后就会被死信交换机通过"dead"这个routingKey路由到死信队列中*/
                parms.put("x-dead-letter-routing-key", "dead");
                /*设定队列的最大长度限制为4, 表示正常队列中最多同时存放4条消息,继续投递过来的消息只能进死信队列*/
                parms.put("x-max-length", 4);
                channel.queueDeclare(NORMAL_QUEUE, false, false, false, parms);
                /*正常队列绑定正常交换机*/
                channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "normal");
        
                DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                    String message = new String(delivery.getBody(), "UTF-8");
                    System.out.println(message);
                };
        
                CancelCallback cancelCallback = consumerTag -> {
                    System.out.println("消费失败");
                };
        
                channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, cancelCallback);
        
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    Producer代码中注释掉设置的消息过期时间,并且设置发布7条消息,修改后代码如下所示

    public class Producer {
        private static final String NORMAL_CHANGE = "normal_exchange";
        public static void main(String[] args) throws IOException, TimeoutException {
            /*获取信道*/
            Channel channel = RabbitmqUtil.getChannel();
            /*设置消息在队列中存活时间*/
            //AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
            /*发布消息*/
            String message = null;
            for (int i=1; i<8; i++){
                message = "commodity" + i;
                channel.basicPublish(NORMAL_CHANGE, "normal", null,  message.getBytes());
                System.out.println("生产者发布消息:" + message);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Consumer2代码无任何修改。

    下面重新进行验证,首先启动Conumer1程序,主要是为先创建normal_queue队列,从浏览器管理端可以查看创建的normal_queue队列,其中该队列有3个属性,Lim表示队列设置了最大长度。
    在这里插入图片描述
    下面为了验证正常队列中4条消息满了后其余消息就会进入死信队列,首先关闭Consumer1,然后启动Producer,输出如下7条消息,表示生产者生产了下述7条消息

    生产者发布消息:commodity1
    生产者发布消息:commodity2
    生产者发布消息:commodity3
    生产者发布消息:commodity4
    生产者发布消息:commodity5
    生产者发布消息:commodity6
    生产者发布消息:commodity7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    查看浏览器管理端如下所示,正常队列中有4条消息,正好是正常队列的最大限度,而死信队列中是3条记录。在这里插入图片描述
    下面启动Consumer2消费掉死信队列中的消息,发现是下面3条消息进入了死信队列

    commodity1
    commodity2
    commodity3
    
    • 1
    • 2
    • 3

    然后再重新启动Consumer1消费掉正常队列中的消息,发现是下面4条消息进入的正常队列。

    commodity4
    commodity5
    commodity6
    commodity7
    
    • 1
    • 2
    • 3
    • 4

    3. 案例三:消息被拒

    还是以上图所示案例,当生产者生产的消息被Consumer1拒绝消费时,被拒绝的消息就会投递到死信队列中。
    假设生产者生产commodity1~commodity7之间的消息,但只有commodity3被拒绝消费。

    下面修改Consumer1代码,只需要在添加一句向RabbitMQ服务器发送basicReject的拒绝指令即可,修改后代码如下

    public class Consumer1 {
        private static final String NORMAL_EXCHANGE = "normal_exchange";      /*正常交换机*/
        private static final String DEAD_EXCHANGE = "dead_exchange";        /*死信交换机*/
        private static final String NORMAL_QUEUE = "normal_queue";          /*正常队列*/
        private static final String DEAD_QUEUE = "dead_queue";              /*死信队列*/
        public static void main(String[] args) throws IOException, TimeoutException {
            /*获取信道*/
            Channel channel = RabbitmqUtil.getChannel();
            /*声明死信交换机和普通交换机*/
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
    
            /*声明死信队列*/
            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
            /*死信队列绑定死信交换机*/
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "dead");
    
            /*声明正常队列, 在声明队列前设置队列的属性*/
            Map<String, Object> parms = new HashMap<>();
            /*设置消息过期时间10s,超过10s后消息就会被投递到死信交换机*/
            //parms.put("x-message-ttl", 10000);
            /*正常队列关联死信交换机, 关联key是固定的, 关联后正常队列中消息无法被消费者消费后就会把消息投递到死信交换机*/
            parms.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            /*为正常队列设置消息失败后投递到死信交换机绑定的routingKey, 这样消息消费后就会被死信交换机通过"dead"这个routingKey路由到死信队列中*/
            parms.put("x-dead-letter-routing-key", "dead");
            /*设定队列的最大长度限制为4, 表示正常队列中最多同时存放4条消息,继续投递过来的消息只能进死信队列*/
            //parms.put("x-max-length", 4);
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, parms);
            /*正常队列绑定正常交换机*/
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "normal");
    
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                if (message.equals("commodity3")){
                    System.out.println("Consumer1拒绝处理消息:" + message);
                    channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); //false表示被拒绝的消息不再放入队列中
                }else {
                    System.out.println(message + "消费成功");
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                }
            };
    
            CancelCallback cancelCallback = consumerTag -> {
                System.out.println("消费失败");
            };
    
            channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, cancelCallback);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    Producer类和Consumer2类继续采用案例二中的

    下面进行测试,在进行测试前首先删除normal_queue队列,然后重新启动Consumer1类,然后再启动Producer进行生产消息,生产者输出如下,表示生产了7条消息

    生产者发布消息:commodity1
    生产者发布消息:commodity2
    生产者发布消息:commodity3
    生产者发布消息:commodity4
    生产者发布消息:commodity5
    生产者发布消息:commodity6
    生产者发布消息:commodity7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再看Consumer1的输出如下所示,表示除了commodity3消息被拒绝处理,其他6个消息都被正常消费了

    ommodity1消费成功
    commodity2消费成功
    Consumer1拒绝处理消息:commodity3
    commodity4消费成功
    commodity5消费成功
    commodity6消费成功
    commodity7消费成功
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再看我们监控端,显示有一个消息被投递到了dead_queue死信队列中
    在这里插入图片描述
    为了确定死信队列中的一个消息是否是被拒绝的commodity3消息,下面启动Consumer2消费死信队列中消息,输出如下所示,表示死信队列中的消息正是被Consumer1拒绝处理的commodity3消息。

    commodity3
    
    • 1
  • 相关阅读:
    【GCC编译优化系列】GCC链接失败的错误提示 undefined reference to ‘xxx‘ 可能还有一种情况你没注意到?
    服务安全-应用协议rsync未授权&ssh漏洞复现
    javaweb-SpringBoot基础
    大赛报名 | 免费体验V853芯片!“华秋电子X全志在线开源硬件设计大赛”开始报名啦
    2022-08-04 Brighthouse: An Analytic DataWarehouse for Ad-hoc Queries
    Docker搭建harbor仓库
    【第十三篇】- Maven 快照(SNAPSHOT)
    AI智能修人像插件 Retouch4me 1.0 九合一
    基于java+springboot+mybatis+vue+elementui的旅游景点门票购票网站
    在Visual Studio/Qt Creator 中使用CMake安装和使用vcpkg包
  • 原文地址:https://blog.csdn.net/u010502101/article/details/125454485