阿里二面:要保證訊息不丟失,又不重複,訊息佇列怎麼選型?

碼農談IT發表於2024-01-16

來源:君哥聊技術

大家好,我是君哥。

在使用訊息佇列時,有兩個經常讓我們煩惱的問題,訊息丟失和訊息重複。那我們在做技術選型時,有沒有一個訊息佇列能解決訊息丟失和訊息重複這兩個問題呢?

訊息丟失

阿里二面:要保證訊息不丟失,又不重複,訊息佇列怎麼選型?

如上圖,從生產者傳送訊息,Broker 儲存訊息,消費者消費訊息,每一個環節都有可能丟失訊息。

傳送丟失

生產者傳送訊息時,如果處理不當,很可能會造成訊息丟失。

生產者傳送訊息,主流訊息佇列都支援同步傳送和非同步傳送。如果使用同步傳送,生產者傳送訊息後,會同步等待 Broker 返回的 ACK,收到 ACK 訊息,就認為訊息傳送成功。如果長時間沒有收到,則會認為訊息傳送失敗,需要進行重試。

同步傳送可以保證訊息不丟失,但是會有效能問題,所以多數情況會選擇非同步傳送。非同步傳送如何保證訊息不丟失呢?主流訊息佇列(比如 Kafka 和 RocketMQ)實現方法基本類似,使用回撥函式來實現。下面看一下 Kafka 的非同步傳送程式碼:

producer.send(record, new Callback() {
 public void onCompletion(RecordMetadata metadata, Exception exception) {
 if (exception != null) {
  logger.error("傳送訊息失敗:", exception);
 }
 if (metadata != null) {
     logger.info("訊息傳送成功");
  }
 }
});

訊息儲存

生產者傳送訊息成功,也不能保證訊息絕對不丟失。因為即使訊息傳送到 Broker,如果在消費者拉取到訊息之前,Broker 當機了,訊息還沒有落盤,也會導致訊息丟失。

在儲存階段要保證訊息不丟失,可以考慮幾個方面:

同步刷盤

採用非同步刷盤,如果在訊息落盤之前 Broker 當機了,就會造成訊息丟失。而採用同步刷盤,等待訊息落盤之後,再給 Sender 返回傳送成功,可以從訊息傳送環節保證訊息不丟失。

阿里二面:要保證訊息不丟失,又不重複,訊息佇列怎麼選型?

在 RocketMQ 中,把 flushDiskType 引數配置為 SYNC_FLUSH 就可以開啟同步刷盤。

Broker 叢集

如果 Broker 叢集中只有一個節點,即使訊息落盤成功了,Broker 傳送故障,在 Broker 恢復以前消費者也會拉取不到訊息。而且如果 Broker 磁碟故障不可恢復,訊息也會丟失。

採用 Broker 叢集可以很好地解決這個問題。見下圖:

阿里二面:要保證訊息不丟失,又不重複,訊息佇列怎麼選型?

在 Broker 叢集時,可以等待 2 個以上的節點同步訊息完成後再給 Producer 返回成功。這樣即使一個 Broker 掛了,也可以很容易找到替代的 Broker。

訊息消費

消費者保證不丟失訊息,需要消費完成後再給 Broker 返回 ACK。在主流的訊息佇列中,如果 Broker 收不到 ACK,都會給消費者再次傳送這條訊息。

有時候為了解決訊息積壓的問題,消費者拉取到訊息後會直接返回 ACK,然後再非同步執行訊息處理邏輯。這樣要保證訊息不丟失,需要在返回 ACK 之前把訊息儲存到本地,比如持久化到資料庫,後面可以取資料庫儲存的訊息進行處理。

訊息重複

訊息重複一般有兩個原因,一個是生產者傳送訊息後沒有收到 ACK,然後進行重複傳送,另一個原因是消費者消費完成後 Broker 沒有收到 ACK,導致訊息重複推送給消費者。

重複訊息會對業務造成影響,比如電商場景中的重複支付、賬務場景中的重複記賬,對業務造成的影響都比較嚴重。

從目前主流的訊息佇列來看,並沒有一個訊息佇列能解決訊息重複消費的問題,只能在消費端做冪等處理。下面提供幾個思路作為參考。

資料庫唯一鍵約束

如果訊息會落本地資料庫,可以採用訊息 ID 作為唯一鍵。如果訊息不落資料庫,可以將訊息 ID 或者訊息中其他唯一能標識訊息的屬性作為唯一鍵落業務資料表。

儲存消費記錄

我們也可以將訊息 ID 儲存 Redis,消費訊息前判斷訊息 ID 是否已存在。

ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
Boolean result = valueOperations.setIfAbsent(messageId, messageId);
if (result) {
 //消費邏輯;
else {
 logger.error("這條訊息已經消費,跳過,訊息ID:{}", messageId);
}

這裡有一個注意點,如果消費失敗了,需要刪除 Redis 中儲存的訊息 ID。

總結

訊息不丟失、不重複是訊息佇列的基本要求,但這個基本要求還是很難滿足的。

訊息丟失這個要求,主流訊息佇列透過訊息重試和訊息持久化的方式可以滿足。

但訊息重試也同時帶來了訊息重複的可能性,主流訊息佇列在解決重複訊息的問題上並沒有現成的方案,對不允許重複消費的場景,需要開發人員在消費端做冪等處理。

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

相關文章