訊息佇列批次收發訊息,請避開這 5 個坑!

碼農談IT發表於2023-12-04

來源:君哥聊技術

大家好,我是君哥。

使用訊息佇列時,為了提高生產和消費的效能,有時會開啟批次處理。

在生產端,生產者傳送的訊息先傳送到一個訊息列表,積累到一定的訊息量之後再批次傳送給 Broker,如下圖:

訊息佇列批次收發訊息,請避開這 5 個坑!

在消費端,消費者拉取訊息後先不立即處理,而是把訊息轉存到一個記憶體佇列或資料庫,由業務執行緒去處理,如下圖:

訊息佇列批次收發訊息,請避開這 5 個坑!

無論是生產者做批次傳送,還是消費者做批次處理,都需要考慮使用批次訊息的業務場景,避免踩坑。下面看一下批次操作可能會遇到哪些坑。

批次大小

當生產者採用批次傳送的方式來提高傳送效能時,一定要考慮傳送訊息的批次大小。下面是 RocketMQ 批次傳送的官方示例:

String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA""OrderID001""Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA""OrderID002""Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA""OrderID003""Hello world 2".getBytes()));
try {
    producer.send(messages);
catch (Exception e) {
    e.printStackTrace();
    //handle the error
}

RocketMQ 預設訊息大小是 4M,由 maxMessageSize 引數控制,如果批次訊息大小超過 maxMessageSize,則會丟擲異常。

如果遇到訊息大小超過 maxMessageSize 的情況時,可以用下面方法進行處理:

  • 把這個引數改大,但需要考慮 Broker 的效能和網路頻寬;
  • 將訊息進行拆分後分批傳送;
  • 對訊息進行壓縮處理。

RabbitMQ 相關的 API 則提供了更加靈活的批次控制,對訊息數量和訊息大小都做了控制,下面看一下原始碼:

訊息佇列批次收發訊息,請避開這 5 個坑!
訊息佇列批次收發訊息,請避開這 5 個坑!

冪等

消費端可以批次拉取訊息進行消費,這樣可以減少拉取訊息時的 RPC 次數,提升消費效能。比如在 RocketMQ 中,可以透過 Consumer 中的 pullBatchSize 來設定一次拉取的訊息數量,透過 consumeMessageBatchMaxSize 引數來設定一次消費的訊息數量。

但需要注意的是,如果批次訊息中一條訊息消費失敗了,這一批訊息都需要進行重試,已經消費成功的訊息會被重複消費,帶來業務問題。

為了不對業務造成影響,必須考慮冪等。一個簡單的方法是在訊息中增加全域性唯一 id 屬性,對訊息消費結果進行記錄,消費成功後儲存 id。這樣在消費訊息之前先查詢是否存在消費成功的記錄,如果存在則直接返回處理成功。

時延

在使用訊息佇列進行批次操作時,必須要考慮到時延問題。比如我們設定一個批次 100 條訊息,積累夠 100 條訊息後再傳送,在訊息量小的情況下,可能積累夠 100 條訊息會很長時間,導致消費端拉取到一條訊息時延很大。

雖然訊息佇列的一個重要作用是削峰填谷,但在一些場景下,對訊息的實時性也有要求。比如在車聯網的充電場景,車聯網平臺需要實時感知充電樁的狀態,如果充電樁積累夠一批訊息再上報平臺,平臺獲取到的狀態會不準確,如果心跳訊息延時太久,平臺會認為充電樁離線。

對於有時延要求又需要批次操作的場景,可以設定一個超時時間,超時後即使訊息數量不夠,也會傳送出去。看下 RabbitMQ 的處理:

public synchronized void send(String exchange, String routingKey, Message message, CorrelationData correlationData)
  throws AmqpException 
{
 if (correlationData != null) {
  //...
  super.send(exchange, routingKey, message, correlationData);
 }
 else {
  if (this.scheduledTask != null) {
   this.scheduledTask.cancel(false);
  }
  MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message);
  if (batch != null) {
   super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null);
  }
  //這裡獲取到超時時間,到達超時時間後使用定時器將訊息傳送出去
  Date next = this.batchingStrategy.nextRelease();
  if (next != null) {
   this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next);
  }
 }
}

可靠性

使用批處理一定要考慮可靠性的問題。

在消費端,消費者批次拉取一批訊息後把訊息暫存到一個記憶體臨時佇列,然後多執行緒去臨時佇列消費訊息,如果服務當機,臨時佇列中的訊息會丟失。

為了避免當機引發的損失,可以拉取一批訊息後儲存到資料庫,然後給 Broker 返回 ACK,之後業務程式碼去資料庫查詢訊息並消費,不過要考慮資料庫大事務、鎖競爭等問題。

當然,對於一些訊息丟失不敏感的場景,比如日誌收集之類的,可靠性這個指標是不用太關注的。

特殊場景

因為批次訊息有一些複雜性,訊息佇列的部分特性不支援。

事務訊息

批次訊息會增加訊息重試的難度,所以對於事務訊息,建議使用單條訊息,一條訊息對應一個事務。

順序訊息

順序訊息的實現思路一般是生產者將訊息傳送到同一個分割槽,消費者繫結這個分割槽並使用單執行緒消費這個分割槽的訊息。如果對同一個 Topic 下的同一個分割槽來實現批次傳送,難度會增大。所以建議順序訊息使用單條訊息進行傳送。

延時訊息

如果延時訊息使用批次進行傳送,這一批訊息的延時時間必須相同,同時要考慮批次訊息的超時時間,超時時間太大會影響延時時間的準確性,生產端實現複雜度大大增加。

總結

使用批次訊息,在一定程度上可以提高效能和吞吐量,但是確實也會存在一些問題,使用的時候要結合業務場景避開這些坑。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2998585/,如需轉載,請註明出處,否則將追究法律責任。