RabbitMQ消息100%可靠性投递的解决方案实现(二)

消费端自定义监听

继承DefaultConsumer,重写handleDelivery()方法

public class Producer {
    public static final String MQ_HOST = "192.168.222.101";
    public static final String MQ_VHOST = "/";
    public static final int MQ_PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        //1. 创建一个ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(MQ_HOST);//配置host
        connectionFactory.setPort(MQ_PORT);//配置port
        connectionFactory.setVirtualHost(MQ_VHOST);//配置vHost

        //2. 通过连接工厂创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        String exchange = "test_consumer_exchange";
        String routingKey = "consumer.save";

        //4. 通过Channel发送数据
        String message = "Hello Consumer Message";

        for (int i = 0; i < 5; i++) {
            channel.basicPublish(exchange,routingKey,null,message.getBytes());
        }
    }
}

public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchange = "test_consumer_exchange";
        String routingKey = "consumer.save";
        String queueName = "test_consumer_queue";

        //2. 声明一个exchange
        channel.exchangeDeclare(exchange,"topic",true,false,null);
        //3. 声明一个队列
        channel.queueDeclare(queueName,true,false,false,null);
        //4. 绑定
        channel.queueBind(queueName,exchange,routingKey);
        //5. 创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        //6. 设置Channel
        channel.basicConsume(queueName,true,new MyConsumer(channel));
       /* //7. 获取消息
       之前的方式,很ugly
        while (true) {
            //nextDelivery 会阻塞直到有消息过来
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println("收到:" + message);
        }*/
    }

    private static class MyConsumer extends DefaultConsumer {
        public MyConsumer(Channel channel) {
            super(channel);
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("——consume message——");
            System.out.println("consumerTag:"+consumerTag);
            System.out.println("envelope:"+envelope);
            System.out.println("properties:"+properties);
            System.out.println("body:"+new String(body));
        }
    }
}


打印如下:

——consume message——
consumerTag:amq.ctag-DLKq_dy8aYspCTUBrnHTew
envelope:Envelope(deliveryTag=1, redeliver=false, exchange=test_consumer_exchange, routingKey=consumer.save)
properties:#contentHeader(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body:Hello Consumer Message
——consume message——
consumerTag:amq.ctag-DLKq_dy8aYspCTUBrnHTew
envelope:Envelope(deliveryTag=2, redeliver=false, exchange=test_consumer_exchange, routingKey=consumer.save)
properties:#contentHeader(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body:Hello Consumer Message
...

消息的限流

消费端的限流:

假设,MQ服务器上有上万条未处理的消息,随便打开一个消费者客户端,会出现下面情况:
巨量的消息瞬间全部推送过来,但是单个客户端无法同时处理这么多数据,从而导致服务器崩溃

解决方案:

  • RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下(设置autoAck为false),若一定数量的消息(通过基于consume或channel设置Qos的值)未被确认前,不消费新的消息

void BasicQos(unit prefetchSize,unshort prefetchCount,bool global)

  • prefetchSize:0 消费的单条消息的大小限制,0代表不限制
  • prefetchCount:不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack
  • global:true\false true:上面设置应用于channel级别; false:应用于consumer级别
public class Producer {
    public static final String MQ_HOST = "192.168.222.101";
    public static final String MQ_VHOST = "/";
    public static final int MQ_PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        //1. 创建一个ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(MQ_HOST);//配置host
        connectionFactory.setPort(MQ_PORT);//配置port
        connectionFactory.setVirtualHost(MQ_VHOST);//配置vHost

        //2. 通过连接工厂创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        String exchange = "test_qos_exchange";
        String routingKey = "qos.save";


        //4. 通过Channel发送数据
        String message = "Hello Qos Message";

        for (int i = 0; i < 5; i++) {
            channel.basicPublish(exchange,routingKey,null,message.getBytes());
        }
    }
}

public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchange = "test_qos_exchange";
        String routingKey = "qos.#";
        String queueName = "test_qos_queue";

        //2. 声明一个exchange
        channel.exchangeDeclare(exchange,"topic",true,false,null);
        //3. 声明一个队列
        channel.queueDeclare(queueName,true,false,false,null);
        //4. 绑定
        channel.queueBind(queueName,exchange,routingKey);
        //5. 创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        //6.限流方式 每次只推3条
        channel.basicQos(0,3,false);
        //7. 设置Channel autoAck一定要设置为false,才能做限流
        channel.basicConsume(queueName,false,new MyConsumer(channel));
        
    }

    private static class MyConsumer extends DefaultConsumer {
        private Channel channel;
        public MyConsumer(Channel channel) {
            super(channel);
            this.channel = channel;
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("——consume message——");
            System.out.println("consumerTag:"+consumerTag);
            System.out.println("envelope:"+envelope);
            System.out.println("properties:"+properties);
            System.out.println("body:"+new String(body));
            // 手动签收
            channel.basicAck(envelope.getDeliveryTag(),false);
        }
    }
}

消费端的ACK与重回队列

消费端可以进行手工ACK和NACK(不确认,表示失败)

  • 消费端进行消费时,如果由于业务异常,可以进行日志记录,然后进行补偿
  • 如果由于服务器宕机等严重问题,需要手工进行ACK保障消费端消费成功

消费端的重回队列:

对没有处理成功的消息,把消息重新传递给Broker
一般在实际应用中,会关闭重回队列

public class Producer {
    public static final String MQ_HOST = "192.168.222.101";
    public static final String MQ_VHOST = "/";
    public static final int MQ_PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        //1. 创建一个ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(MQ_HOST);//配置host
        connectionFactory.setPort(MQ_PORT);//配置port
        connectionFactory.setVirtualHost(MQ_VHOST);//配置vHost

        //2. 通过连接工厂创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        String exchange = "test_ack_exchange";
        String routingKey = "ack.save";

        //4. 通过Channel发送数据
        String message = "Hello Ack Message";

        for (int i = 0; i < 5; i++) {
            Map headers = new HashMap<>();
            headers.put("num",i);
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) //持久化
                    .contentEncoding("UTF-8")
                    .headers(headers)
                    .build();
            channel.basicPublish(exchange,routingKey,properties,message.getBytes());
        }
    }
}
public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchange = "test_ack_exchange";
        String routingKey = "ack.#";
        String queueName = "test_ack_queue";

        //2. 声明一个exchange
        channel.exchangeDeclare(exchange,"topic",true,false,null);
        //3. 声明一个队列
        channel.queueDeclare(queueName,true,false,false,null);
        //4. 绑定
        channel.queueBind(queueName,exchange,routingKey);
        //5. autoAck = false
        channel.basicConsume(queueName,false,new MyConsumer(channel));


    }

    private static class MyConsumer extends DefaultConsumer {
        private Channel channel;
        public MyConsumer(Channel channel) {
            super(channel);
            this.channel = channel;
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("——consume message——");
            System.out.println("body:"+new String(body));
            System.out.println("num:" + properties.getHeaders().get("num"));
            if ((Integer)properties.getHeaders().get("num") == 0 ) {
                //requeue:true表示重新入队,重传
                channel.basicNack(envelope.getDeliveryTag(),false,true);
            } else {
                channel.basicAck(envelope.getDeliveryTag(),false);
            }

        }
    }
}

日志:

...
——consume message——
body:Hello Ack Message
num:0
——consume message——
body:Hello Ack Message
num:0
——consume message——
body:Hello Ack Message
num:0
...

num为0的消息会一直回到MQ队列的最尾端,然后一直循环打印,因为一直无法消费

TTL消息

  • 生存时间(Time to Live,TTL),指的是消息的生成时间
  • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
  • 还支持队列的过期时间,从消息入队列开始计时,只要超过了队列的超时时间,那么消息会自动的清除
 AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) //2:持久化投递;1:非持久化(未消费的消息重启后就没了)
                .contentEncoding("UTF-8")
                .expiration("5000")//5s 设置消息的TTL
                .headers(headers)
                .build();
      
            String message = "Hello";
            channel.basicPublish("","testQueue",properties,message.getBytes());
        }

队列TTL:

Map args = new HashMap<>();  
args.put("x-expires", 1800000);  //30分钟
channel.queueDeclare("myqueue", false, false, false, args); 

死信队列

  • 死信队列(Dead-Letter-Exchange,DLX)
  • 利用DLX,当消息在队列中变成死信(dead message:没有消费者去消费)之后,它能被重新publish到另一个Exchange,这个Exchange就是死信队列
  • DLX是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定
  • 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上,进而被路由到另一个队列
  • 可以监听这个队列中的消息并做相应的处理

消息变成死信的情况

  1. 消息被拒绝(basic.rejcet/basic.nack)且requeue=false
  2. 消息TTL过期
  3. 队列达到最大长度

消息中间件消费到的消息处理失败怎么办?
专门有一个后台线程,监控消费者(如 物流)系统是否正常,能否请求的,不停的监视。
一旦发现消费者系统恢复正常,这个后台线程就从死信队列中取出 消费处理失败的消息,重新执行相应逻辑。

死信队列设置

1.首先要设置死信队列的exchange和queue,然后进行绑定:

  • Exchange:dlx.exchange
  • Queue:dlx.queue
  • RoutingKey:#

2.进行正常声明交换机、队列、绑定,只不过需要在队列上加上一个参数:(consumer里添加)arguments.put("x-dead-letter-exchange","dlx.exchange);
3.消息在过期、requeue、队列达到最大长度时,消息就可以直接路由到死信队列


public class Producer {
    public static final String MQ_HOST = "192.168.222.101";
    public static final String MQ_VHOST = "/";
    public static final int MQ_PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        //1. 创建一个ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(MQ_HOST);//配置host
        connectionFactory.setPort(MQ_PORT);//配置port
        connectionFactory.setVirtualHost(MQ_VHOST);//配置vHost

        //2. 通过连接工厂创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        String exchange = "test_dlx_exchange";
        String routingKey = "dlx.save";


        //4. 通过Channel发送数据
        String message = "Hello DLX Message";

        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) //2:持久化投递;1:非持久化(未消费的消息重启后就没了)
                .contentEncoding("UTF-8")
                .expiration("5000")//5s后如果没有消费端消费,会变成死信
                .build();

        for (int i = 0; i < 1; i++) {
            channel.basicPublish(exchange,routingKey,properties,message.getBytes());
        }

    }
}

public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchange = "test_dlx_exchange";
        String routingKey = "dlx.#";
        String queueName = "test_dlx_queue";

        String dlxExchange = "dlx.exchange";
        String dlxQueue = "dlx.queue";

        //2. 声明一个exchange
        channel.exchangeDeclare(exchange,"topic",true,false,null);
        Map arguments = new HashMap<>();

        //路由失败,重发到dlx.exchange
        arguments.put("x-dead-letter-exchange",dlxExchange);
        /**
         * 声明正常队列
         * arguments要设置到声明队列上
         */
        channel.queueDeclare(queueName,true,false,false,arguments);
        channel.queueBind(queueName,exchange,routingKey);

        //进行死信队列的声明
        channel.exchangeDeclare(dlxExchange,"topic",true,false,null);
        channel.queueDeclare(dlxQueue,true,false,false,null);
        channel.queueBind(dlxQueue,dlxExchange,"#");

    }

    private static class MyConsumer extends DefaultConsumer {
        public MyConsumer(Channel channel) {
            super(channel);
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("——consume message——");
            System.out.println("body:"+new String(body));
        }
    }
}

先启动Consumer,进行生成队列等操作
RabbitMQ消息100%可靠性投递的解决方案实现(二)_第1张图片

RabbitMQ消息100%可靠性投递的解决方案实现(二)_第2张图片
接着,为了让消息变成死信,停止Consumer

最后,启动Producer
RabbitMQ消息100%可靠性投递的解决方案实现(二)_第3张图片
5秒后,消息没被消费,然后就进去了死信队列(注意死信队列中初始值是1)
RabbitMQ消息100%可靠性投递的解决方案实现(二)_第4张图片

转自:RabbitMQ学习——高级特性

你可能感兴趣的:(消息中间件)