深入RabbitMQ高级特性

文章目录

  • 保证消息100%投递
    • 如何保证生产者可靠性投递消息
    • 方案一 : 消息入库,对消息状态进行打标。
    • 方案二 : 消息延迟投递,进行二次确认,回调检查
  • confirm确认消息
  • Return消息机制
  • 消费端限流策略
  • 消费端ACK与重回队列
    • 消费端的手动ACK和NACK
    • 消费端重回队列
  • TTL队列/消息
  • 死信队列
    • 死信队列设置

保证消息100%投递

如何保证生产者可靠性投递消息

  • 保证消息成功发出
  • 保障MQ节点成功接受
  • 发送端收到MQ节点(Broker)确认应答
  • 对消息进行补偿机制

方案一 : 消息入库,对消息状态进行打标。

深入RabbitMQ高级特性_第1张图片

  • step1:业务数据入库,消息数据入库(两次操作DB 高并发可能会受影响)
  • step2:producer向MQ发送消息
  • step3:producer异步监听MQ确认消息投递到队列
  • step4:producer监听到确认送达后,修改消息数据库状态
  • step5:补偿机制,通过定时任务轮询扫描消息表中,未确认的消息。
  • step6:补偿机制,将未确认的消息从新发送。
  • step7:补偿机制,记录重试次数,达到一定数值后,将消息状态该为失败。(通过人工处理)

方案二 : 消息延迟投递,进行二次确认,回调检查

异步操作,性能更高
深入RabbitMQ高级特性_第2张图片
整个流程需要三个消息队列来完成。

  • step1:先业务数据入库,然后发送业务消息。
  • step2:延迟发送第二条check消息。(延迟时间根据业务来确定)
  • step3:消费端监听队列并消费消息。
  • step4:消费端发送confirm消息。(此消息为从新发送的新消息在不同的队列中,非ack签收)
  • step5:回调服务监听消费端的confirm消息队列,并将消息进行持久化存储。
  • step6:回调服务监听check消息队列,查询数据库,如果业务消息没有消费,回调服务将告诉producer从新发送业务消息

confirm确认消息

  • 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
  • 生产者进行接受应答,用来确认这条消息是否正常的发送到Broker,这种方式是消息可靠性投递的核心保障
    深入RabbitMQ高级特性_第3张图片
  • 第一步:在channel上开启消息确认模式:channel.confirmSelect();
  • 第二步:在channel上添加监听,addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续操作。
        //生产者
        //省略 创建连接和channel

        //打开消息确认模式
        channel.confirmSelect();

        String exchange = "test.confirm.exchange";
        String routingKey = "confirm.routing-key";
        String message = "hello rabbitmq";
        channel.basicPublish(exchange, routingKey, null, message.getBytes());
        //添加确认监听
        channel.addConfirmListener(new ConfirmListener() {
            //消息成功发送回调
            public void handleAck(long deliverTag, boolean multiple) throws IOException {
                System.err.println("----发送成功---");
            }
            //消息发送失败回调
            public void handleNack(long deliverTag, boolean multiple) throws IOException {
            	//队列满、磁盘满 等等情况
                System.err.println("----发送失败---");
            }
        });
        //消费者
        //省略 创建连接和channel
        String queueName = "confirm-queue";
        String exchange = "test.confirm.exchange";
        String routingKey = "confirm.#";
        //声明交换机
        channel.exchangeDeclare(exchange, "topic");
        //声明队列
        channel.queueDeclare(queueName, false, false, false, null);
        //队列、交换机、routing key 三者绑定
        channel.queueBind(queueName, exchange, routingKey);

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.err.println(message);
            }
        };
        channel.basicConsume(queueName, true, defaultConsumer);
    }

Return消息机制

  • Return Listener 用于处理一些不可路由的消息
  • 我们的生产者,通过指定一个Exchange和Routing key,把消息送达到某个消息队列,然后我们的消费者监听队列,进行消费操作。
  • 但是在某些情况下,如果我们在发送消息的时候,当前的Exchange不存在或者指定的Routing key路由不到,此时我们需要监听这种不可达消息,就是用Return Listener。

深入RabbitMQ高级特性_第4张图片

Mandatory: 如果为true,则监听器会收到路由不可达的消息,然后进行处理,如果为false,Broker会自动删除消息。

        //生产者
        //省略 创建连接和channel
        String exchange = "test.return.exchange";
        String routingKey = "return.routing-key";
        String message = "hello rabbitmq";

        //声明交换机  不绑定队列 触发return 回调
        channel.exchangeDeclare(exchange, "topic");

        //添加 return 监听器
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode, String replyTest, String exchange, String routingKey,
                                     AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                System.err.println("replyCode: " + replyCode);
                System.err.println("replyTest: " + replyTest);
                System.err.println("exchange: " + exchange);
                System.err.println("routingKey: " + routingKey);
                System.err.println("basicProperties: " + basicProperties);
                System.err.println("body: " + new String(body, "UTF-8"));
            }
        });
        //第三个参数 是否打开 return 机制
        channel.basicPublish(exchange, routingKey, true,null, message.getBytes());
    }

消费端限流策略

什么是消费端限流?

  • 假设一个场景,首先,我们RabbitMQ服务器上有上万条未处理的消息,随便打开一个消费端,就会有巨量消息推送过来,压垮客户端。
  • RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动签收消息的前提下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)未被确认前,不进行新消息的消费。
  • 在自动签收消息的情况下 不生效

void BasicQos(int prefetchSize,short prefetchCount,boolean global);
prefetchSize:消息的大小限制,0 不限制。
prefetchCount:推送的消息数目。
global:true\false 是否将以上设置 应用于channel上
简单点说:就是上面限制是channel级别还是consumer级别

        //生产者
        //省略 创建连接和channel

        String exchange = "test.qos.exchange";
        String routingKey = "qos.routing-key";
        String message = "hello rabbitmq";
        for (int i = 0; i < 9; i++) {
            channel.basicPublish(exchange, routingKey, null, message.getBytes());

        }
        channel.close();
        connection.close();
        //消费者
        //省略 创建连接和channel
        String queueName = "qos-queue";
        String exchange = "test.qos.exchange";
        String routingKey = "qos.routing-key";
        //声明交换机
        channel.exchangeDeclare(exchange, "topic");
        //声明队列
        channel.queueDeclare(queueName, false, false, false, null);
        //队列、交换机、routing key 三者绑定
        channel.queueBind(queueName, exchange, routingKey);

        // 限流 第一件事:autoAck 设置为false
        channel.basicQos(0, 3, false);
        channel.basicConsume(queueName, false, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.err.println(message);
                //第二个参数  是否批量签收
                channel.basicAck(envelope.getDeliveryTag(), true);
            }
        });      

消费端ACK与重回队列

ACK:签收,NACK不签收

消费端的手动ACK和NACK

  • 消费端进行消费是,如果业务出现异常NACK消息,经历三到四次重试后依然异常,手动ACK,然后进行补偿。

消费端重回队列

  • 为了将没有处理成功的消息重新投递到Queue的队尾中。
  • 实际项目中,应用较少。
        //生产者
        //省略 创建连接和channel

        String exchange = "test.ack.exchange";
        String routingKey = "ack.routing-key";

        for (int i = 0; i < 5; i++) {
            Map<String, Object> headers = new HashMap<>();
            headers.put("index", i);
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) //持久化
                    .headers(headers)
                    .build();

            String message = "hello rabbitmq " + i;
            channel.basicPublish(exchange, routingKey, properties, message.getBytes());
        }
        //消费者
        //省略 创建连接和channel
        String queueName = "ack-queue";
        String exchange = "test.ack.exchange";
        String routingKey = "ack.routing-key";
        //声明交换机
        channel.exchangeDeclare(exchange, "topic");
        //声明队列
        channel.queueDeclare(queueName, false, false, false, null);
        //队列、交换机、routing key 三者绑定
        channel.queueBind(queueName, exchange, routingKey);

        //必须手工签收  关闭autoACK
        channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.err.println(message);
                if ((Integer) properties.getHeaders().get("index") == 0) {
                    //第三个参数  是否重回队列
                    channel.basicNack(envelope.getDeliveryTag(), false, true);
                }else{
                    //第二个参数  是否批量签收
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            }
        });

TTL队列/消息

  • TTL 是 Time To Live 的缩写,也就是生存时间
  • RabbitMQ支持消息的过期时间设置,在消息发送时指定
  • RabbitMQ支持队列的过期时间设置,从消息入队时开始计算,只要超过队列的超时时间配置,消息自动清除

死信队列

消息变成死信有以下几种情况

  • 消息被拒绝(basic.reject/basic.nack)并且requeue = false。
  • 消息TTL过期
  • 队列达到最大长度
  • 当消息在一个队列中变成私信之后,它会被重新public到另外一个Exchange中,这个Exchange就是DLX(私信队列)
  • DLX也是一个正常的Exchange,和一般的Exchange没有区别,它可以在任何的队列指定,实际上就是设置队列的属性。
  • 当这个队列中有私信时,RabbitMQ就会自动的将这个消息重新发送到设置的Exchange上去,进而被路由到另一个队列

死信队列设置

  • 首先需要设置私信队列的Exchange和queue,然后进行绑定。

例如:
Exchange:dlx.exchange
Queue:dlx.queue
Routing key:#

  • 然后我们正常声明Exchange、Queue、binding,只不过我们需要在队列上加上一个参数即可:arguments.put(“x-dead-letter-exchange”,“dlx.exchange”)
  • 简单点说:就是在队列上设置私信队列就可以。
        //生产者
        //省略 创建连接和channel
        String exchange = "test.dlx.exchange";
        String routingKey = "dlx.routing-key";

        //进行私信队列的声明
        channel.exchangeDeclare("dlx.exchange", "topic", true, false, false, null);
        channel.queueDeclare("dlx.queue", false, false, false, null);
        channel.queueBind("dlx.queue", "dlx.exchange", "#");

        //声明普通队列和交换机
        String queueName = "test.dlx-queue";
        channel.exchangeDeclare(exchange, "topic");

        //-----------------------设置队列的私信队列---------------------------------------
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "dlx.exchange");
        //这个arguments属性要设置到声明队列上
        channel.queueDeclare(queueName, false, false, false, arguments);
        //-----------------------设置队列的私信队列---------------------------------------
        
        channel.queueBind(queueName, exchange, routingKey);

        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .expiration("1000") //设置过期时间  不启动消费者 使消息变为私信
                .build();
        String message = "hello rabbitmq";
        channel.basicPublish(exchange, routingKey,properties, message.getBytes());
        channel.close();
        connection.close();

深入RabbitMQ高级特性_第5张图片

你可能感兴趣的:(深入RabbitMQ高级特性)