RabbitMQ學習(三)之 “訊息佇列高階使用”

依劍行走天下發表於2020-12-20

上一篇文章介紹了RabbitMQ的基本使用,這篇文章總結RabbitMQ的高階使用方法

1.TTL(Time o To Live) 訊息過期時間

有兩種設定方式

  1. 通過佇列屬性設定訊息過期時間
    通過佇列屬性設定訊息過期時間所有佇列中的訊息超過時間未被消費時,都會過期。
@Bean("ttlQueue")
public Queue queue() {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("x-message-ttl", 11000); // 佇列中的訊息未被消費 11 秒後過期
    return new Queue("TTL_QUEUE", true, false, false, map);
}
  1. 設定單條訊息的過期時間
    在傳送訊息的時候指定訊息屬性。
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("4000"); // 訊息的過期屬性,單位 ms
Message message = new Message("這條訊息 4 秒後過期".getBytes(), messageProperties);
rabbitTemplate.send("TTL_EXCHANGE", "bread.ttl", message);

如果同時指定了 Message TTL 和 Queue TTL,則小的那個時間生效。

2.死信佇列

訊息在某些情況下會變成死信 (Dead Letter)
佇列在建立的時候可以指定一個死信交換機 DLX(Dead Letter Exchange)
死信交換機繫結的佇列被稱為死信佇列 DLQ(Dead Letter Queue),DLX 實際上
也是普通的交換機,DLQ 也是普通的佇列(例如替補球員也是普通球員)。

什麼情況下訊息會變成死信?

  1. 訊息被消費者拒絕並且未設定重回佇列:(NACK || Reject ) && requeue== false
  2. 訊息過期
  3. 佇列達到最大長度,超過了 Max length(訊息數)或者 Max length bytes(位元組數),最先入隊的訊息會被髮送到 DLX。

死信佇列如何使用?

  1. 宣告原交換機(GP_ORI_USE_EXCHANGE)、原佇列(GP_ORI_USE_QUEUE),相互繫結。佇列中的訊息 10 秒鐘過期,因為沒有消費者,會變成死信。指定原佇列的死信交換
    (GP_DEAD_LETTER_EXCHANGE)。
@Bean("oriUseExchange")
public DirectExchange exchange() {
	return new DirectExchange("ORI_USE_EXCHANGE", true, false, new HashMap<>());
}@Bean("oriUseQueue")
public Queue queue() {
	Map<String, Object> map = new HashMap<String, Object>();
	map.put("x-message-ttl", 10000); // 10 秒鐘後成為死信
	map.put("x-dead-letter-exchange", "DEAD_LETTER_EXCHANGE"); // 佇列中的訊息變成死信後,進入死信交換機
	return new Queue("ORI_USE_QUEUE", true, false, false, map);
}@Bean
	public Binding binding(@Qualifier("oriUseQueue") Queue queue,@Qualifier("oriUseExchange") DirectExchange
	exchange) {
	return BindingBuilder.bind(queue).to(exchange).with("ori.use");
}
  1. 宣告死信交換機 ( DEAD_LETTER_EXCHANGE ) 、 死信佇列(DEAD_LETTER_QUEUE),相互繫結
@Bean("deatLetterExchange")
	public TopicExchange deadLetterExchange() {
	return new TopicExchange("DEAD_LETTER_EXCHANGE", true, false, new HashMap<>());
}@Bean("deatLetterQueue")
	public Queue deadLetterQueue() {
	return new Queue("DEAD_LETTER_QUEUE", true, false, false, new HashMap<>());
}

@Bean
public Binding bindingDead(@Qualifier("deatLetterQueue") Queue queue,@Qualifier("deatLetterExchange")
TopicExchange exchange) {
	return BindingBuilder.bind(queue).to(exchange).with("#"); // 無條件路由
}
  1. 最終消費者監聽死信佇列。
  2. 生產者傳送訊息。
    示意圖
    在這裡插入圖片描述

3. 延遲佇列

我們在實際業務中有一些需要延時傳送訊息的場景,例如:

  1. 家裡有一臺智慧熱水器,需要在 30 分鐘後啟動
  2. 未付款的訂單,15 分鐘後關閉

RabbitMQ 本身不支援延遲佇列,總的來說有三種實現方案:

  1. 先儲存到資料庫,用定時任務掃描
  2. 利用 RabbitMQ 的死信佇列(Dead Letter Queue)實現
  3. 利用 rabbitmq-delayed-message-exchange 外掛

1.TTL+DLX實現延遲佇列

流程上面程式碼和圖示已經講過了

訊息的流轉流程:
生產者——原交換機——原佇列(超過 TTL 之後)——死信交換機——死信佇列—
—最終消費者

使用死信佇列實現延時訊息的缺點:

  1. 如果統一用佇列來設定訊息的 TTL,當梯度非常多的情況下,比如 1 分鐘,2分鐘,5 分鐘,10 分鐘,20 分鐘,30 分鐘……需要建立很多交換機和佇列來路由訊息。
  2. 如果單獨設定訊息的 TTL,則可能會造成佇列中的訊息阻塞——前一條訊息沒有出隊(沒有被消費),後面的訊息無法投遞(比如第一條訊息過期 TTL 是 30min,第二條訊息 TTL 是 10min。10 分鐘後,即使第二條訊息應該投遞了,但是由於第一條訊息還未出隊,所以無法投遞)。
  3. 可能存在一定的時間誤差。

2. 基於延遲佇列外掛的實現(Linux)

在 RabbitMQ 3.5.7 及 以 後 的 版 本 提 供 了 一 個 插 件(rabbitmq-delayed-message-exchange)來實現延時佇列功能。同時外掛依賴Erlang/OPT 18.0 及以上。

外掛原始碼地址:
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
外掛下載地址:
https://bintray.com/rabbitmq/community-plugins/rabbitmq_delayed_message_exchange

  1. 進入外掛目錄
whereis rabbitmq
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins
  1. 下載外掛
wget
https://bintray.com/rabbitmq/community-plugins/download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.e
z

如果下載的檔名帶問號則需要改名

mv download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.ez
rabbitmq_delayed_message_exchange-0.0.1.ez
  1. 啟用外掛
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  1. 停用外掛
rabbitmq-plugins disable rabbitmq_delayed_message_exchange
  1. 外掛使用
    通過宣告一個 x-delayed-message 型別的 Exchange 來使用 delayed-messaging特性。x-delayed-message 是外掛提供的型別,並不是 rabbitmq 本身的(區別於 direct、topic、fanout、headers)
@Bean("delayExchange")
public TopicExchange exchange() {
	Map<String, Object> argss = new HashMap<String, Object>();
	argss.put("x-delayed-type", "direct");
	return new TopicExchange("DELAY_EXCHANGE", true, false, argss);
}

生產者:
訊息屬性中指定 x-delay 引數。

MessageProperties messageProperties = new MessageProperties();
// 延遲的間隔時間,目標時刻減去當前時刻
messageProperties.setHeader("x-delay", delayTime.getTime() - now.getTime());
Message message = new Message(msg.getBytes(), messageProperties);// 不能在本地測試,必須傳送訊息到安裝了外掛的 Linux 服務端
rabbitTemplate.send("DELAY_EXCHANGE", "#", message);

4. 服務端流控(Flow Control)

當 RabbitMQ 生產 MQ 訊息的速度遠大於消費訊息的速度時,會產生大量的訊息堆積,佔用系統資源,導致機器的效能下降。我們想要控制服務端接收的訊息的數量,應該怎麼做呢?佇列有兩個控制長度的屬性:

  • x-max-length:佇列中最大儲存最大訊息數,超過這個數量,隊頭的訊息會被丟棄。
  • x-max-length-bytes:佇列中儲存的最大訊息容量(單位 bytes),超過這個容量,隊頭的訊息會被丟棄。

需要注意的是,設定佇列長度只在訊息堆積的情況下有意義,而且會刪除先入隊的訊息,不能真正地實現服務端限流。

有沒有其他辦法實現服務端限流呢?

4.1 記憶體控制

RabbitMQ 會在啟動時檢測機器的實體記憶體數值。預設當 MQ 佔用 40% 以上記憶體時,MQ 會主動丟擲一個記憶體警告並阻塞所有連線(Connections)。可以通過修改rabbitmq.config 檔案來調整記憶體閾值,預設值是 0.4,如下所示:

[{rabbit, [{vm_memory_high_watermark, 0.4}]}].

也可以用命令動態設定,如果設定成 0,則所有的訊息都不能釋出。

rabbitmqctl set_vm_memory_high_watermark 0.3

4.2 磁碟控制

另一種方式是通過磁碟來控制訊息的釋出。當磁碟空間低於指定的值時(預設50MB),觸發流控措施。
例如:指定為磁碟的 30%或者 2GB:

disk_free_limit.relative = 3.0
disk_free_limit.absolute = 2GB

5. 消費端限流

預設情況下,如果不進行配置,RabbitMQ 會盡可能快速地把佇列中的訊息傳送到消費者。因為消費者會在本地快取訊息,如果訊息數量過多,可能會導致 OOM 或者影響其他程式的正常執行。

在消費者處理訊息的能力有限,例如消費者數量太少,或者單條訊息的處理時間過長的情況下,如果我們希望在一定數量的訊息消費完之前,不再推送訊息過來,就要用到消費端的流量限制措施。

可以基於 Consumer 或者 channel 設定 prefetch count 的值,含義為 Consumer端的最大的 unacked messages 數目。當超過這個數值的訊息未被確認,RabbitMQ 會停止投遞新的訊息給該消費者。

channel.basicQos(2); // 如果超過 2 條訊息沒有傳送 ACK,當前消費者不再接受佇列訊息
channel.basicConsume(QUEUE_NAME, false, consumer);

Spring Boot 配置:

spring.rabbitmq.listener.simple.prefetch=2

舉例:
channel 的 prefetch count 設定為 5。當消費者有 5 條訊息沒有給 Broker 傳送 ACK後,RabbitMQ 不再給這個消費者投遞訊息。

相關文章