RabbitMQ學習筆記:惰性隊列(Lazy Queues)

理論說明

RabbitMQ從3.6.0開始引入了惰性隊列(Lazy Queue)的概念。惰性隊列會盡可能的將消息存入磁盤中,而在消費者消費到相應的消息時纔會被加載到內存中,它的一個重要設計目標是能夠支持更長的隊列,即支持更多的消息存儲。當消費者由於各種各樣的原因(比如消費者下線、跌機、或者由於維護而關閉等)致使長時間不能消費消息而造成堆積時,惰性隊列就很必要了。

默認情況下,當生產者將消息發送到RabbitMQ的時候,隊列中的消息會盡可能地存儲在內存之中,這樣可以更加快速地將消息發送給消費者。即使是持久化的消息,在被寫入磁盤的同時也會在內存中駐留一份備份。當RabbitMQ需要釋放內存的時候,會將內存中的消息換頁至磁盤中,這個操作會耗費較長的時間,也會阻塞隊列的操作,進而無法接收新的消息。雖然RabbitMQ的開發者們一直在升級相關的算法,但是效果始終不太理想,尤其是在 消息量特別大的時候。

惰性隊列會將接收到的消息直接存入文件系統,而不管是持久化的或者是非持久化的,這樣可以減少內存的消耗,但是會增加I/O的使用,如果消息是持久化的,那麼這樣的I/O操作不可避免,惰性隊列和持久化的消息可謂是“最佳拍檔”。注意如果惰性隊列中存儲的是非持久化的消息,內存的使用率會一直很穩定,但是重啓之後消息一樣會丟失。

隊列具備兩種模式:default和lazy。默認的爲default模式,在3.6.0的版本無需做任何變更。lazy模式即爲惰性隊列的模式,可以通過調用channel.queueDeclare方法的時候在參數中設置,也可以通過Policy的方式設置,如果一個隊列同時使用這兩種方式設置,那麼Policy的方式具備更高的優先級。如果要通過聲明的方式改變已有隊列的模式,那麼只能先刪除隊列,然後再重新聲明一個新的。

下面的示例展示瞭如何聲明一個惰性隊列:

  Map<String, Object> args = new HashMap<String, Object>();
  args.put("x-queue-mode", "lazy");
  channel.queueDeclare("myqueue", false, false, false, args);

使用Policy設置一個隊列爲惰性隊列:

rabbitmqctl rabbitmqctl set_policy Lazy “^lazy-queue$” ‘{“queue-mode”:“lazy”}’ --apply-to queues
rabbitmqctl(Windows) rabbitmqctl set_policy Lazy “^lazy-queue$” “{”“queue-mode”":"“lazy”"}" --apply-to queues

惰性隊列和普通隊列相比,只有很小的內存開銷。這裏很難對每種情況給出一個具體的數值,但是我們可以類比一下:發送1千萬條消息,每條消息的大小爲1KB,並且此時沒有任何的消費者,那麼普通隊列會消耗1.2GB內存,而惰性隊列只能消耗1.5MB的內存。

根據官方測試數據顯示,對於普通隊列,如果要發送1千萬條消息,需要耗費801秒,平均發送速度約爲13000條/秒。如果使用惰性隊列,那麼發送同樣多的消息時,耗時是421秒,平均發送速度約爲24000條/秒。出現性能偏差的原因是普通隊列會由於內存不足而不得不將消息換頁至磁盤。如果有消費者消費時,惰性隊列會耗費將近40MB的空間來發送消息,對於一個 消費者的情況,平均的消費速度約爲14000條/秒。

如果要將普通隊列轉變爲惰性隊列,我們需要忍受同樣的性能損耗,首先需要將緩存中的消息換頁至磁盤中,然後才能接收新的消息。反之,當將一個惰性隊列轉變爲一個普通隊列的時候,和恢復一個隊列執行同樣的操作,會將磁盤中的消息批量的導入到內存中。

示例
1.聲明隊列、交換器、綁定關係
package com.yaomy.control.rabbitmq.amqp.lazy.config;

import com.google.common.collect.Maps;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

/**
 * @Description: RabbitMQ生產者交換器、綁定、隊列聲明
 * @Version: 1.0
 */
@SuppressWarnings("all")
@Configuration
public class RabbitLazyConfig {
    public static final String LAZY_TOPIC_EXCHANGE = "lazy.topic.exchange";
    public static final String LAZY_TOPIC_QUEUE = "lazy_topic_queue";
    public static final String LAZY_TOPIC_ROUTING_KEY = "*.topic.*";
    /**
     * 聲明隊列
     */
    @Bean
    public Queue topicLazyQueue(){
        Map<String, Object> args = Maps.newHashMap();

        args.put("x-queue-mode", "lazy");
        /**
         * 設置持久化隊列
         */
        return QueueBuilder.durable(LAZY_TOPIC_QUEUE).withArguments(args).build();
    }


    /**
     * 聲明Topic類型交換器
     */
    @Bean
    public TopicExchange topicLazyExchange(){
        TopicExchange exchange = new TopicExchange(LAZY_TOPIC_EXCHANGE);
        return exchange;
    }

    /**
     * Topic交換器和隊列通過bindingKey綁定
     * @return
     */
    @Bean
    public Binding bindingTopicLazyExchangeQueue(){
        return BindingBuilder.bind(topicLazyQueue()).to(topicLazyExchange()).with(LAZY_TOPIC_ROUTING_KEY);
    }

}

2.生產者
package com.yaomy.control.rabbitmq.amqp.lazy;

import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.connection.PublisherCallbackChannel;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * @Description: RabbitMQ生產者
 * @ProjectName: spring-parent
 * @Version: 1.0
 */
@SuppressWarnings("all")
@Component
public class RabbitLazySender {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 創建一個消息是否投遞成功的回調方法
     */
    private final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
        /**
         *
         * @param correlationData 消息的附加信息
         * @param ack true for ack, false for nack
         * @param cause 是一個可選的原因,對於nack,如果可用,否則爲空。
         */
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            if(!ack){
                //可以進行日誌記錄、異常處理、補償處理等
                System.err.println("異常ack-"+ack+",id-"+correlationData.getId()+",cause:"+cause);
            }else {
                //更新數據庫,可靠性投遞機制
                System.out.println("正常ack-"+ack+",id-"+correlationData.getId());
                try{
                System.out.println(new String(correlationData.getReturnedMessage().getBody()));

                } catch (Exception e){

                }
            }
        }
    };
    /**
     * 創建一個消息是否被隊列接收的監聽對象,如果沒有隊列接收發送出的消息,則調用此方法進行後續處理
     */
    private final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
        /**
         *
         * @param message 被退回的消息
         * @param replyCode 錯誤編碼
         * @param replyText 錯誤描述
         * @param exchange 交換器
         * @param routingKey 路由
         */
        @Override
        public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
            System.err.println("spring_returned_message_correlation:"+message.getMessageProperties().getHeaders().get(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY)
                                +"return exchange: " + exchange
                                + ", routingKey: "+ routingKey
                                + ", replyCode: " + replyCode
                                + ", replyText: " + replyText
                                + ",message:" + message);
            try {
                System.out.println(new String(message.getBody()));
            } catch (Exception e){

            }
        }
    };
    /**
     * 擴展點,在消息轉換完成之後,發送之前調用;可以修改消息屬性、消息頭信息
     */
    private final MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            MessageProperties properties = message.getMessageProperties();
            /**
             * 設置消息發送到隊列之後多久被丟棄,單位:毫秒
             * 此種方案需要每條消息都設置此屬性,比較靈活;
             * 還有一種方案是在聲明隊列的時候指定發送到隊列中的過期時間;
             * * Queue queue = new Queue("test_queue2");
             * * queue.getArguments().put("x-message-ttl", 10000);
             * 這兩種方案可以同時存在,以值小的爲準
             */
            //properties.setExpiration("10000");
            /**
             * 設置消息的優先級
             */
            properties.setPriority(9);
            /**
             * 設置消息發送到隊列中的模式,持久化|非持久化(只存在於內存中)
             */
            properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

            return message;
        }
    };
    /**
     * 發送消息
     * @param exchange 交換器
     * @param route 路由鍵
     * @param message 消息
     * @param properties
     */
    public void sendMsg(String exchange, String routingKey, String message, MessageProperties properties){
        /**
         * 設置生產者消息publish-confirm回調函數
         */
        this.rabbitTemplate.setConfirmCallback(confirmCallback);
        /**
         * 設置消息退回回調函數
         */
        this.rabbitTemplate.setReturnCallback(returnCallback);
        /**
         * 新增消息轉換完成後、發送之前的擴展點
         */
        this.rabbitTemplate.setBeforePublishPostProcessors(messagePostProcessor);

        try {
            if(null == properties){
                properties = new MessageProperties();
            }
            /**
             * 設置消息唯一標識
             */
            properties.setMessageId(UUID.randomUUID().toString());
            /**
             * 創建消息包裝對象
             */
            Message msg = MessageBuilder.withBody(message.getBytes()).andProperties(properties).build();
            /**
             * 將消息主題和屬性封裝在Message類中
             */
            Message returnedMessage = MessageBuilder.withBody(message.getBytes()).build();
            /**
             * 相關數據
             */
            CorrelationData correlationData = new CorrelationData();
            /**
             * 消息ID,全局唯一
             */
            correlationData.setId(msg.getMessageProperties().getMessageId());

            /**
             * 設置此相關數據的返回消息
             */
            correlationData.setReturnedMessage(returnedMessage);
            /**
             * 如果msg是org.springframework.amqp.core.Message對象的實例,則直接返回,否則轉化爲Message對象
             */
            this.rabbitTemplate.convertAndSend(exchange, routingKey, msg, correlationData);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

3.消費者
package com.yaomy.control.rabbitmq.amqp.lazy;

import com.rabbitmq.client.Channel;
import com.yaomy.control.rabbitmq.amqp.lazy.config.RabbitLazyConfig;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

/**
 * @Description: RabbitMQ消息消費者
 * @Version: 1.0
 */
@SuppressWarnings("all")
@Component
public class RabbitLazyReceiver {
    /**
     *
     * @param channel 信道
     * @param message 消息
     * @throws Exception
     */
    @RabbitListener(queues = RabbitLazyConfig.LAZY_TOPIC_QUEUE)
    public void onMessage(Channel channel, Message message) throws Exception {
        System.out.println("--------------------------------------");
        System.out.println("消費端Payload: " + message.getPayload()+"-ID:"+message.getHeaders().getId()+"-messageId:"+message.getHeaders());
        Long deliveryTag = (Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
        //手工ACK,獲取deliveryTag
        channel.basicAck(deliveryTag, false);
    }
}

參考:https://www.rabbitmq.com/lazy-queues.html
GitHub地址:https://github.com/mingyang66/spring-parent/tree/master/spring-boot-control-rabbitmq-service

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章