消息中间件RabbitMQ(三):进阶特性

RabbitMQ进阶

mandatory和immediate

我们在学习basicPublish方法中提到过这两个参数,但是没有讲解。
这两个参数的作用是当消息传递过程中不可到达目的地时将消息返回给生产者。

mandatory

当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设为false时,消息则会被直接丢弃。

生产者可以通过调用channel.addReturnListener方法添加监听器来获取到没有被路由到合适队列的消息。

public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("exchange0710","direct",false,true,null);
        channel.queueDeclare("queue0710",false,false,true,null);
        channel.queueBind("queue0710","exchange0710","key0710");
        //注意要把mandatory设置为true
        channel.basicPublish("exchange0710","key", true,MessageProperties.PERSISTENT_TEXT_PLAIN,"helloworld".getBytes());
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message=new String(body);
                System.out.println("交换器为"+exchange+",路由键为"+routingKey+",返回的消息为"+message);
            }
        });
        connection.close();
    }

消息中间件RabbitMQ(三):进阶特性_第1张图片

immediate

当immediate参数设为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回给生产者。 简单来说:imrnediate参数告诉服务器,如果该消息关联的队列上有消费者, 则立刻投递:,如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了。

RabbitMQ3.0版本以后去掉了对immediate参数的支持,RabbitMQ的官方解释是:immediate参数会影响镜像队列的性能,增加了代码复杂性。

所以当我们在使用basicPublish方法时如果使用immediate参数的话,就会出现异常。
但是在后面我们会学习到替代的方法。

备份交换器

RabbitMQ提供的备份变换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。

生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失,如果设置了mandatory参数,那么需要添加监听器来实现逻辑,生产者的代码会更加复杂。如果不想复杂化生产者的代码,又不想消息丢失,就可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中,需要的时候再去处理这些消息。

可以通过在声明交换器时添加alternate-exchange参数来实现,也可以通过策略(后面会讲)的方式实现,前者优先级更高。

接下来看一个例子,首先我们声明了两个交换器normalExchange和myAE,分别绑定了normalQueue和unroutedQueue队列,同时将myAE设置为normalExchange的备份交换器。

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //设置参数,备份交换器的名字
        Map arg=new HashMap<>();
        arg.put("alternate-exchange","myAE");
        //记得将参数传入正常的交换器
        channel.exchangeDeclare("normalExchange","direct",false,false,arg);
        //声明备份交换器,这里我们设置类型为fanout
        channel.exchangeDeclare("myAE","fanout",false,false,null);
        //声明正常队列和备份交换器的队列,并将交换器和队列分别绑定
        channel.queueDeclare("normalQueue",false,false,false,null);
        channel.queueDeclare("unroutedQueue",false,false,false,null);
        channel.queueBind("normalQueue","normalExchange","normalKey");
        channel.queueBind("unroutedQueue","myAE","");
        //发送消息
        for (int i=0;i<10;i++) {
            if(i%2==0) {
                channel.basicPublish("normalExchange", "normalKey", MessageProperties.PERSISTENT_TEXT_PLAIN,("第"+i+"条消息").getBytes());
            }else {
                channel.basicPublish("normalExchange", "xxx", MessageProperties.PERSISTENT_TEXT_PLAIN,("第"+i+"条消息").getBytes());
            }
        }
        connection.close();
    }

当我们发送路由键为"normalKey"的消息时,消息能被正确路由到normalQueue队列中。如果路由键为其他值,则因为找不到匹配的队列,而将消息发送给myAE,进而进入unroutedQueue队列中。

    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //消费备份交换器中的消息
        channel.basicConsume("unroutedQueue",false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException
            {
                System.out.println("消费消息"+new String(body));
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
        TimeUnit.SECONDS.sleep(20);
        connection.close();
    }

消息中间件RabbitMQ(三):进阶特性_第2张图片

备份交换器和普通的交换器其实没什么区别,设置为fanout类型可以更方便的使用,不用考虑路由键的情况。当类型为direct或者topic之类的时候,需要注意,消息被重新发送到备份交换器的时候路由键和从生产者发出的路由键是一样的。

备份交换器有以下几种特殊情况

  • 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都没有异常,消息丢失。
  • 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都没有异常,消息丢失。
  • 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都没有异常,消息丢失。
  • 如果备份交换器和mandatory参数一起使用,则mandatory参数无效。

过期时间(TTL)

RabbitMQ可以对消息和队列设置过期时间。
目前有两种方法可以设置消息的过期时间。第一种是通过队列的属性设置,队列中的所有消息都有相同的过期时间。第二种是对消息本身进行单独设置,这样可以针对性的设置。如果两种方式一起使用,则哪个时间小哪个生效。

通过队列属性设置消息TTL

通过队列属性设置消息TTL是在channel.queueDeclare方法中加入"x-message-ttl"参数实现的,单位是毫秒。

   public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        Map map=new HashMap<>();
        //设置过期时间
        map.put("x-message-ttl",20000);
        channel.exchangeDeclare("TTLexchange","direct",false,true,null);
        channel.queueDeclare("TTLqueue",false,false,true,map);
        channel.queueBind("TTLqueue","TTLexchange","TTLkey");
        for (int i=0;i<5;i++) {
            channel.basicPublish("TTLexchange", "TTLkey", MessageProperties.PERSISTENT_TEXT_PLAIN, ("第"+i+"条消息").getBytes());
        }
        connection.close();
    }

消息中间件RabbitMQ(三):进阶特性_第3张图片
我们发现20S后消息过期消失了。
消息中间件RabbitMQ(三):进阶特性_第4张图片
如果将TTL设置为0,则表示除非此时可以直接将消息投递给消费者,否则该消息会被立即抛弃,是不是觉得有点熟悉?这个和之前的immediate参数有点类似,但是immediate参数会将方法返回给生产者,这个却是直接丢弃。我们后面会有解决的方法

单条消息设置TTL

针对单条消息设置TTL的方法是通过channel.basicPublish方法中设置expiration的属性参数实现的,单位是毫秒。

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("TTLexchange","direct",false,true,null);
        channel.queueDeclare("TTLqueue",false,false,true,null);
        channel.queueBind("TTLqueue","TTLexchange","TTLkey");
        //我们先发送三条6秒的消息,再发送4条10分钟的消息,最后再发送三条6秒的
        for (int i=0;i<10;i++) {
            if(i<3) {
                channel.basicPublish("TTLexchange", "TTLkey",
                        new AMQP.BasicProperties().builder().expiration("6000").build()
                        , ("第" + i + "条消息").getBytes());
            }else if(i<7) {
                channel.basicPublish("TTLexchange", "TTLkey",
                        new AMQP.BasicProperties().builder().expiration("600000").build()
                        , ("第" + i + "条消息").getBytes());
            }else {
                channel.basicPublish("TTLexchange", "TTLkey",
                        new AMQP.BasicProperties().builder().expiration("6000").build()
                        , ("第" + i + "条消息").getBytes());
            }
        }
        connection.close();
    }

按照我们预期来想,六秒后,队列里应该只剩四条消息,但是管理台的显示却和我们想象的不同。

消息中间件RabbitMQ(三):进阶特性_第5张图片

我们发现等了5分钟,有两条6秒就应该过时的消息却一直没消失,那么为什么会造成这种现象呢?

对于第一种设置队列TTL属性的方法,一旦消息过期就会立刻从队列中删除。而第二种方法,消息过期却不会马上从队列中删除,因为每条消息是否过期是在即将投递到消费者之前判定的,也就是在队列头的时候会判断,因为我们先发送了三条6秒过期的消息,所以三条6秒过期的消息在队列的最前方,判断过期之后就删除,而接下来的消息是十分钟的,所以就轮不到后面的消息来判断,即会暂时保存在队列中。

为什么要这样设计呢?因为第一种方法里,所有的过期时间是统一设置的,只要从头删除就ok了。而第二种方法,每条消息的过期时间都不一样,如果要删除过期消息则需要定期扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除。

如果两者混用的话同样根据第二种方法来判断。

我们消费一下,果然只有四条消息。
消息中间件RabbitMQ(三):进阶特性_第6张图片

死信队列

DLX(Dead-Letter-Exchange),可以称为死信交换器。当消息在一个队列中变成死信(dead message)后,它能够被重新发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就被称为死信队列。

消息变成死信一般由于以下几种情况:

  • 消息被拒绝,并且设置requeue参数为false
  • 消息过期
  • 队列达到最大长度

DLX其实也是一个正常的交换器,它能在任何队列上被指定,即设置一个属性。当这个队列存在死信时,RabbitMQ会自动将这个消息重新发送到设置的DLX上去,进而被路由到死信队列,然后可以对消息进行相应处理。
死信队列与消息的TTL设置为0配合使用就能实现immediate参数的功能。

通过在channel.queueDeclare方法中设置"x-dead-letter-exchange"参数来为这个队列添加DLX。
可以通过设置"x-dead-letter-routing-key"参数来指定发送消息时携带的路由键,如果不指定,则消息是原本队列的路由键。

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //声明两个队列,一个正常的,一个死信队列
        channel.exchangeDeclare("exchange.dlx","direct");
        channel.exchangeDeclare("exchange.normal","fanout");
        Map map=new HashMap<>();
        //设置过期时间
        map.put("x-message-ttl",10000);
        //设置死信队列
        map.put("x-dead-letter-exchange","exchange.dlx");
        //设置重发消息时的路由键
        map.put("x-dead-letter-routing-key","dlxkey");
        channel.queueDeclare("queue.normal",false,false,true,map);
        channel.queueBind("queue.normal","exchange.normal","");
        channel.queueDeclare("queue.dlx",false,false,false,null);
        channel.queueBind("queue.dlx","exchange.dlx","dlxkey");
        channel.basicPublish("exchange.normal","xxxx",MessageProperties.PERSISTENT_TEXT_PLAIN,"死信".getBytes());
        connection.close();
    }

我们首先声明了两个交换器并绑定了两个队列,在RabbitMQ的管理台可以看到。正常的队列设置了TTL、DLX、DLK等属性。
消息中间件RabbitMQ(三):进阶特性_第7张图片然后我们发送了一条路由键为"xxxx"的消息,经过交换器exchange.normal进入到queue.normal队列,然后因为设置了10s的过期时间,在10s内没有消费者消费这条消息,当消息过时的时候,消息会被重新发送给exchange.dlx,并且路由键设置为dlxkey,然后消息进入到死信队列。

消息中间件RabbitMQ(三):进阶特性_第8张图片

延迟队列

延迟队列存储的对象是延迟消息,"延迟消息"是指当消息被发送之后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到消息进行消费。

延迟队列的使用场景例如:

  • 订单系统中,一个用户下单之后通常有十五分钟的时间进行支付,如果十五分钟内没有支付成功,这个订单将进行异常处理,可以使用延迟队列来处理这些订单。
  • 用户通过手机让智能家居在指定的时间进行工作,这时就可以将指令存在延迟队列中,到时间再推送到智能家居。

RabbitMQ本身并没有直接支持延迟队列的功能,但是可以通过DLX和TTL实现延迟队列的功能。

其实我们前面死信队列的例子中,已经具备了延迟队列功能。对于queue.dlx这个死信队列来说,其实可以将其看成延迟队列。当应用需要将每条消息都设置十秒的延迟,消费者就订阅queue.dlx队列,消息在进入queue.normal队列十秒后过期,然后进入queue.dlx队列,就正好被消费者消费到,就实现了延迟队列的功能。

优先级队列

优先级队列可以先通过设置"x-max-priority"参数来设置队列的最大优先级,然后再在消息中设置每条消息的优先级,优先级高的可以被优先消费。

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setUri("amqp://admin:[email protected]:5672");
        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        Map map=new HashMap<>();
        //设置最大优先级为10
        map.put("x-max-priority",10);
        channel.exchangeDeclare("exchange.priority","direct",false,true,null);
        channel.queueDeclare("queue.priority",false,false,true,map);
        channel.queueBind("queue.priority","exchange.priority","key");
        for (int i=1;i<=10;i++){
        	//设置消息的优先级
            channel.basicPublish("exchange.priority","key",
                    new AMQP.BasicProperties().builder().priority(i).build(),("第"+i+"条").getBytes());
            //i为3,6,9的消息各自再发送一条最高优先级的消息
            if((i==3)||(i==6)||(i==9)){
                channel.basicPublish("exchange.priority","key",
                        new AMQP.BasicProperties().builder().priority(10).build(),("第"+i+"条").getBytes());
            }
        }
        connection.close();
    }

消息中间件RabbitMQ(三):进阶特性_第9张图片

如果消费者的消费速度大于生产者的速度且Broker中没有消息堆积的情况下,对发送的消息设置优先级就没有什么意义,因为生产者刚发完消息就被消费者消费了,Broker中最多也只有一条消息。

你可能感兴趣的:(RabbitMQ)