十一、消息对队列和消费确认

消息到队列

消息发送后,如何确定到消息是否到达了相应的队列?RabbitMQ默认在发送消息时,如果不能根据交换器类型和路由键找到相应的队列,消息将直接丢弃。而要想知道消息是否达到队列或没到队列却不想消息丢失,RabbitMQ提供有解决方案:

  • 设置mandatory参数

    • true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,消息返回给生产者;
    • false时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,消息直接丢弃。RabbitMQ默认。

    可以通过调用channel.addReturnListener来添加ReturnListener监听器获取没有被正确路由到队列的消息:

    public class Send {
        final static String EXCHANGE = "mandatoryParam";
        public static void main(String[] args) {
            Connection connection = null;
            Channel channel;
            try {
                connection = ConnectionUtils.getConnection();
                channel = connection.createChannel();
                channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT);
                //消息发送时,设置mandatory参数为true
                channel.basicPublish(EXCHANGE, "", true, null, "test".getBytes("utf-8"));
    		   //监听消息是否路由到队列,没路由到将接收到返回的消息
                channel.addReturnListener(new ReturnListener() {
                    @Override
                    public void handleReturn(int replyCode, 
                                             String replyText, 
                                             String exchange, 
                                             String routingKey, 
                                             AMQP.BasicProperties properties, 
                                             byte[] body) throws IOException {
                        System.out.println("没找到符合的队列而退回的消息 = " + new String(body, "utf-8"));
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }finally {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    当发送消息不能成功路由到队列时,ReturnListener会监听到返回的"test"消息并打印输出。

  • 设置immediate参数

    消息关联的队列上有消费者,就投递;如果所有匹配的队列上都没有消费者,则将消息返回给生产者。

    • true时,如果所有匹配的队列上都没有消费者,则将消息返回给生产者。
    • false时,队列有没有消费者都投递。

    RabbitMQ3.0版本开始已经不支持对imediate参数的支持(建议采用TTL和DLX),了解即可。

  • 备份交换器

    生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失。

    如果设置了mandatory参数,那么需要添加ReturnListener的编程逻辑,生产者的代码将变得复杂。

    如果既不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由到队列的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。

    备份交换器的使用有两种方法:

    • 在调用channel.exchangeDeclare方法时添加alternate-exchange参数来实现

      消息发送客户端代码:

      public class Send {
          final static String exchange = "正常交换器";
          final static String exchange2 = "备份交换器";
          final static String queue = "正常队列";
          final static String queue2 = "备份队列";
      
          public static void main(String[] args) {
              Connection connection = null;
              Channel channel;
              try {
                  connection = ConnectionUtils.getConnection();
                  channel = connection.createChannel();
      
                  //要使用备份交换器exchange2,必须要设置该参数
                  Map<String, Object> param = new HashMap<>();
                  param.put("alternate-exchange", exchange2);
      
                  //创建正常交换器,消息正常路由到队列
                  channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, true, false, param);
                  channel.queueDeclare(queue, true, false, false, null);
                  channel.queueBind(queue, exchange, exchange);
      
                  //创建备份路由器,消息不能正常路由到队列后,则消息发送到该备份路由器从而路由到备份队列
                  channel.exchangeDeclare(exchange2, BuiltinExchangeType.FANOUT, true, false, null);
                  channel.queueDeclare(queue2, true, false, false , null);
                  channel.queueBind(queue2, exchange2, "");
      
                  //测试不能正常路由到队列
                  channel.basicPublish(exchange, 
                                       "无效的路由键", 
                                       MessageProperties.PERSISTENT_TEXT_PLAIN, 
                                       "备份交换器".getBytes("utf-8"));
              } catch (IOException e) {
                  e.printStackTrace();
              } catch (TimeoutException e) {
                  e.printStackTrace();
              }finally {
                  try {
                      connection.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      

      如上面发送消息代码所示,发送到交换器的消息因为RoutingKey不匹配不能正常路由到队列,如果没有设置mandatory参数或备份交换器,消息将会直接丢弃。而现在使用了备份交换器,消息将重新发送到备份交换器,备份交换器再路由到备份队列中,从而消息不会丢失。

      对于备份交换器,和普通交换器一样,支持交换器四大类型。建议使用fanout类型,因为消息被发送到备份交换器的路由键和从生产者发出的路由键是一样的,如果使用direct类型,则备份交换器和备份队列的路由键和生产者指定的路由键一样,但如果生产者因为RoutingKey错误导致不能路由到正常队列,这时,备份路由器就不能预知到生产者错误路由键和备份路由器的路由键匹配而不能路由到备份队列也会导致消息丢失。而使用fanout类型,即使生产者错误的路由键也能路由到备份队列中。

    • 也可以通过策略(Policy,后面说明使用)的方法实现。

      命令行输入:

      rabbitmqctl set_policy AE "^正常交换器$" '{"alternate-exchange":"备份交换器"}'
      

    如果两者同时使用,则前者比后者优先级高,会覆盖掉Policy的设置。

    即使设置了备份交换器,以下几种情况仍然会导致消息:

    • 设置的备份交换器不存在
    • 备份交换器没有绑定任何队列
    • 备份交换器没有任何匹配的队列

    如果备份交换器和mandatory参数一起使用,mandatory参数将无效。

消费确认

在上面程序中,如果RabbitMQ一旦向消费客户端发送消息后,立即将其标记删除。在这时,刚好消费客户端接收到还没处理,突然因为其他原因而死掉,此时就出现消息会永久丢失的情况。

为了确保消息不会丢失,要使用RabbitMQ的消息确认。消费者发出ack(nowledgement)告诉RabbitMQ已接收并处理,RabbitMQ再删除。

RabbitMQ消息确认:如果消费者死亡(通道关闭、连接关闭或TCP连接丢失等)而不发送确认给RabbitMQ,RabbitMQ将理解消息未完全处理并将重新进入队列,消息将重新发送,并且该消息不会超时失效。

要使用消息确认,要打开手动消息确认。在前面的工作队列的实例,在channel.basicConsume(QUEUE, true, consumer);中,autoAck = true为自动消息确认,需要设置autoAck = false关闭自动确认来使用手动消息确认。

设置autoAck = true后,设置消息确认有三个方法:

  • channel.basicAck(long deliveryTag, boolean requeue):用于消息成功接收

    参数:

    • deliveryTag:RabbitMQ发送消息的标签,是单调递增的正整数,可通过envelope.getDeliveryTag()获取
    • requeue:
      • true时,RabbitMQ将自动确认所有指定小于deliveryTag的消息,例如:在Channel上传递过来的deliveryTag标签有5678,当消费者先接收到标签8并确认,则小于8的有567都全部确认。
      • false时,对于上面的例子,只有8确认,其他不会确认。
  • channel.basicNack(long deliveryTag, boolean multiple,boolean requeue):用于拒绝接收(批量)

    参数:

    • deliveryTag:RabbitMQ发送消息的标签,是单调递增的正整数,可通过envelope.getDeliveryTag()获取

    • multiple:

      • true,则表示拒绝deliveryTag编号以下所有未被当前消费者确认的消息。

      • false,则表示拒绝编号为deliveryTag的这一条消息,这时候basicNackbasicReject方法一样;

    • requeue:被拒绝的消息是否重新入队列。true时,重新入队列重发给下一个消费者;false时,立即把消息删除,不再会发给消费者

  • channel.basicReject(long deliveryTag, boolean requeue):用于拒绝接收(单条)

    参数意义同上

消息成功确认

生产者客户端发送消息

/**
 * @author Hayson
 * @date 2018/11/23 13:39
 * @description rabbitmq生产者发送多条消息
 */
public class Send {
    final static String QUEUE = "helloWorld";
    public static void main(String[] args) throws IOException, TimeoutException {
        send();
    }
    public static void send() throws IOException, TimeoutException {
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        for (int i = 0; i < 5; i++) {
            String message = "Hello World! " + i;
            //发送消息,指定发送交换器(""则为自带默认交换器)
            channel.basicPublish("", QUEUE, null, message.getBytes("UTF-8"));
            System.out.println("发送消息:" + message);
        }
        channel.close();
        connection.close();
    }
}

消费者客户端成功接收消息:

/**
 * @author Hayson
 * @date 2018/11/23 13:41
 * @description rabbitmq消费者接收消息2
 */
public class Receiver2 {
    final static String QUEUE = "helloWorld";
    public static void main(String[] args) throws IOException, TimeoutException {
        recevier();
    }
    public static void recevier() throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        
        //关闭自动确认
        boolean autoAck = false;
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, 
                                       Envelope envelope, 
                                       AMQP.BasicProperties properties, 
                                       byte[] body)throws IOException {
                //获取标签
                long deliveryTag = envelope.getDeliveryTag();
                //为false, 确认只接收当前deliveryTag
                channel.basicAck(deliveryTag, false);
                //为true, 确认接收所有小于等于deliveryTag的消息
                //channel.basicAck(deliveryTag, true);
                
                String message = new String(body, "UTF-8");
                System.out.println("接收到消息:" + message);
            }
        };
        //关闭自动确认
        channel.basicConsume(QUEUE, autoAck, consumer);
        //channel.close();
        //connection.close();
    }
}

上面消费者客户端代码中,autoAck = falsemultiple = false表示确认了成功接收了当前deliveryTag的消息。autoAck = falsemultiple = true表示确认接收所有小于等于deliveryTag的消息。

如果设置了autoAck = false,而不确认接收消息时,例如注释上面消费者客户端代码channel.basicAck(deliveryTag, false);后,在消费者接收到消息后不确认返回给生产者时,生产者不会删除队列中当前的消息并标记为unAcked

消息拒绝确认

  • 对于上面消费者客户端代码,修改如下:

    boolean autoAck = false;
    //继承DefaultConsumer类来实现消费,获取消息
    Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body)throws IOException {
            long deliveryTag = envelope.getDeliveryTag();
            // 消费者拒绝当前标签消息确认,并重新进入队列发送
            //channel.basicNack(deliveryTag, false, true);
            // 消费者拒绝当前标签消息确认,并删除队列消息
            channel.basicNack(deliveryTag, false, false);
    
            String message = new String(body, "UTF-8");
            System.out.println("接收到消息:" + message);
        }
    };
    channel.basicConsume(QUEUE, autoAck, consumer);
    

    上面消息拒绝确认使用了channel.basicNack方法,当然也可以使用channel.basicReject方法。消费者拒绝消息后,如果标记了重新进入队列发送,有可能会重新发送给当前消费者,也有可能发送给其他消费者。如果拒绝后不需要的消息,可以标记直接删除。

posted @ 2018-12-04 17:18  Hayson  阅读(278)  评论(0编辑  收藏  举报