Redis 學習筆記(六)Redis 如何實現訊息佇列

Ethan_Wong發表於2022-02-12

一、訊息佇列

訊息佇列(Messeage Queue,MQ)是在分散式系統架構中常用的一種中介軟體技術,從字面表述看,是一個儲存訊息的佇列,所以它一般用於給 MQ 中間的兩個元件提供通訊服務。

1.1 訊息佇列介紹

我們引入一個削峰填谷實際場景來介紹 MQ ,削峰填谷是指處理短時間內爆發的請求任務,將巨量請求任務“削峰”,平攤在平常請求任務較低的時間段,也就是“填谷”。 比如元件1 釋出請求任務,元件2接受請求任務並處理。如果沒有 MQ , 元件2 就會在大量的請求任務下會出現假死的情況:

而如果使用 MQ 後可以將這些請求先暫存到佇列中,排隊執行,就不會出現元件2 假死的情況了。我們一般把傳送訊息的元件稱為生產者,接受訊息的元件稱為消費者,如下圖展示一個訊息佇列的模型:

訊息佇列需要滿足訊息有序性、能處理重複的訊息以及訊息可靠性,這樣才能保證存取訊息的一致性。

  • 訊息有序性:雖然消費者非同步讀取訊息,但是要按照生產者傳送訊息的順序來處理訊息,避免後傳送的訊息被先處理掉。
  • 重複訊息處理:在訊息佇列存取資訊時,有可能因為網路阻塞而出現訊息重傳的情況。可能會造成業務邏輯被多次執行,所以要避免重複訊息的處理。
  • 訊息可靠性:在元件故障時,比如消費者當機或者沒有處理完資訊時,訊息佇列需要能提供訊息可靠性保證。所以需要在消費者故障時,可以重新讀取訊息再次進行處理,不影響業務服務。

1.2 訊息佇列應用場景

主要的應用有:非同步處理、流量削峰、系統解耦

1.2.1 商品秒殺

秒殺活動中,會短時間出現爆發式的使用者請求,如果沒有訊息佇列,會導致伺服器響應不過來。輕則會導致服務假死;重則會讓伺服器直接當機。

這時可以加上訊息佇列,伺服器接收到使用者的請求後,先把這些請求全部寫入訊息佇列中再排隊處理,這樣就不會導致同時處理多個請求的情況;若訊息佇列長度超過承載的最大數量,可以拋棄後續的訊息,給使用者返回“頁面出錯,請重新重新整理”提示,這樣降低伺服器的負載,而且也能給使用者很好的互動體驗。

1.2.2 系統解耦

此外,我們可以利用訊息佇列來把系統的業務功能模組化,實現系統功能的解耦。如下圖:

如果有兩個功能服務,而且關係不是很緊密,比如訂單系統和優惠券,雖然都和使用者有關聯,但是如果都放在使用者模組,面臨功能刪減時會很麻煩。所以採用把兩個服務獨立出來,而將兩個服務的訊息傳送以約定的方式通過訊息佇列傳送過去,讓其對應的消費者分別處理即可達到系統解耦的目的。

1.3 常見的訊息佇列中介軟體

1.3.1 RabbitMQ

1.3.1.1 RabbitMQ 介紹

RabbitMQ 是一個老牌的開源訊息中介軟體,它實現了標準的 AMQP(Advanced Message Queuing Protocol,高階訊息佇列協議)訊息中介軟體,使用 Erlang 語言開發,支援叢集部署。支援 java、python、Go、.NET 等等主流開發語言。

其主要的執行流程如下圖:

我們發現在 Rabbit 伺服器中,它在生產者和佇列間加入了交換器(ExChange)模組,它的作用和交換機很相似,它會根據配置的路由規則將生產者發出的訊息分發到不同的佇列中。路由規則很靈活,可以自己來進行設計。

1.3.1.2 RabbitMQ 特點
  1. 支援持久化,RabbitMQ 支援磁碟持久化功能,保證了訊息不會丟失;
  2. 高併發,RabbitMQ 使用了 Erlang 開發語言,Erlang 是為電話交換機開發的語言,天生自帶高併發光環和高可用特性;
  3. 支援分散式叢集,正是因為 Erlang 語言實現的,因此 RabbitMQ 叢集部署也非常簡單,只需要啟動每個節點並使用 --link 把節點加入到叢集中即可,並且 RabbitMQ 支援自動選主和自動容災;
  4. 支援多種語言,比如 Java、.NET、PHP、Python、JavaScript、Ruby、Go 等;
  5. 支援訊息確認,支援訊息消費確認(ack)保證了每條訊息可以被正常消費;
  6. 它支援很多外掛,比如網頁控制檯訊息管理外掛、訊息延遲外掛等,RabbitMQ 的外掛很多並且使用都很方便。

因為中間中的交換器模組,所以RabbitMQ 有不同的訊息型別,主要分為以下幾種:

  • direct(預設型別)模式,此模式為一對一的傳送方式,也就是一條訊息只會傳送給一個消費者;
  • headers 模式,允許你匹配訊息的 header 而非路由鍵(RoutingKey),除此之外 headers 和 direct 的使用完全一致,但因為 headers 匹配的效能很差,幾乎不會被用到;
  • fanout 模式,為多播的方式,會把一個訊息分發給所有的訂閱者;
  • topic 模式,為主題訂閱模式,允許使用萬用字元(#、*)匹配一個或者多個訊息,我可以使用“cn.mq.#”匹配到多個字首是“cn.mq.xxx”的訊息,比如可以匹配到“cn.mq.rabbit”、“cn.mq.kafka”等訊息。

但是 Rabbit 也存在以下的問題:

  • RabbitMQ 對訊息堆積的支援並不好,當大量訊息積壓的時候,會導致 RabbitMQ 的效能急劇下降。
  • RabbitMQ 的效能是這幾個訊息佇列中最差的,大概每秒鐘可以處理幾萬到十幾萬條訊息。如果應用對訊息佇列的效能要求非常高,那不要選擇 RabbitMQ。
  • RabbitMQ 使用的程式語言 Erlang,擴充套件和二次開發成本高

1.3.2 Kafka

1.3.2.1 Kafka 介紹

Kafka 是 LinkedIn 公司開發的基於 ZooKeeper 的多分割槽、多副本的分散式訊息系統,它於 2010 年貢獻給了 Apache 基金會,並且成為了 Apache 的頂級開源專案。其中 ZooKeeper 的作用是用來為 Kafka 提供叢集後設資料管理以及節點的選舉和發現等功能。

與 RabbitMQ 不同中間的 Kafka 叢集部分是由 Broker 代理和 ZooKeeper 叢集組成:

1.3.2.2 Kafka 特點
  • Kafka 與周邊生態系統的相容性是最好的沒有之一,尤其在大資料和流計算領域,幾乎所有的相關開源軟體系統都會優先支援 Kafka。
  • Kafka 效能高效、可擴充套件良好並且可持久化。它的分割槽特性,可複製和可容錯都是不錯的特性。
  • Kafka 使用 Scala 和 Java 語言開發,設計上大量使用了批量和非同步的思想,使得 Kafka 能做到超高的效能。Kafka 的效能,尤其是非同步收發的效能,是三者中最好的,但與 RocketMQ 並沒有量級上的差異,大約每秒鐘可以處理幾十萬條訊息。
  • 在有足夠的客戶端併發進行非同步批量傳送,並且開啟壓縮的情況下,Kafka 的極限處理能力可以超過每秒 2000 萬條訊息。

同時 Kafka 也有缺點:

  • Kafka 同步收發訊息的響應時延較高。因為其非同步批量的設計帶來的問題,在它的 Broker 中,很多地方都會使用這種先攢一波再一起處理的設計。當你的業務場景中,每秒鐘訊息數量沒有那麼多的時候,Kafka 的時延反而會比較高。所以,Kafka 不太適合線上業務場景。

1.3.3 RocketMQ

1.3.3.1 RocketMQ 介紹

RocketMQ 是阿里巴巴開源的分散式訊息中介軟體,用 Java 語言實現,在設計時參考了 Kafka,並做出了自己的一些改進,後來捐贈給 Apache 軟體基金會。支援事務訊息、順序訊息、批量訊息、定時訊息、訊息回溯等。它裡面有幾個區別於標準訊息中件間的概念,如Group、Topic、Queue等。系統組成則由Producer、Consumer、Broker、NameServer等

RocketMQ 要求生產者和消費者必須是一個叢集。叢集級別的高可用,是RocketMQ 和其他 MQ 的區別。

  • Name Server(名稱服務提供者) :是一個幾乎無狀態節點,可叢集部署,節點之間沒有任何資訊同步。提供命令、更新和發現 Broker 服務
  • Broker (訊息中轉提供者):負責儲存轉發訊息
    • broker分為 Master Broker 和 Slave Broker,一個 Master Broker 可以對應多個 Slave Broker,但是一個 Slave Broker 只能對應一個 Master Broker。
1.3.3.2 RocketMQ 特點
  • 是一個佇列模型的訊息中介軟體,具有高效能、高可靠、高實時、分散式等特點
  • Producer 向一些佇列輪流傳送訊息,佇列集合稱為 Topic,Consumer 如果做廣播消費,則一個 Consumer 例項消費這個 Topic 對應的所有佇列,如果做叢集消費,則多個 Consumer 例項平均消費這個 Topic 對應的佇列集合
  • RocketMQ 的效能比 RabbitMQ 要高一個數量級,每秒鐘大概能處理幾十萬條訊息
  • RocketMQ 的劣勢是與周邊生態系統的整合和相容程度不夠。

二、Redis 如何實現訊息佇列

2.1 基於List 實現訊息佇列

List 的先進先出其實就符合訊息佇列對訊息有序性的需求。具體實現如下圖:

但是,在生產者往 List 中寫入資料時,List 訊息集合並不會主動地通知消費者有新訊息寫入。所以 Redis 提供了 brpop 命令, brpop 命令也稱為阻塞式讀取,客戶端在沒有讀到佇列資料時,自動阻塞,直到有新的資料寫入佇列,再開始讀取新資料。此外,訊息佇列通過給每一個訊息提供全域性唯一的 ID 號來解決分辨重複訊息的需求。而訊息的最後一個需求,訊息可靠性如何解決呢?為了留存訊息,List 型別提供brpoplpush 命令來讓消費者從一個 List 中讀取訊息,同時, Redis 會把這個訊息再插入到另一個 List 中留存。這樣如果消費者處理時發生當機,再次重啟時,也可以從備份 List 中重新讀取訊息並進行處理。如下圖:

2.2 基於釋出訂閱實現訊息佇列

Redis 主要有兩種釋出/訂閱模式:基於頻道(channel)和基於模式(pattern)的釋出/訂閱。

2.2.1 基於頻道的釋出/訂閱

在 Redis 2.0 之後 Redis 就新增了專門的釋出和訂閱的型別,Publisher(釋出者)和 Subscriber(訂閱者)來實現訊息佇列了,它們對應的執行命令如下:

# 釋出訊息
publish channel "message"
# 訂閱訊息
subscribe channel
# 取消訂閱
unsubscribe channel

2.2.2 基於模式的釋出/訂閱

除了訂閱頻道外,客戶端還可以通過 psubscribe 命令訂閱一個或者多個模式,從而成為這些模式的訂閱者,它還會被髮送給所有與這個頻道相匹配的模式的訂閱者,命令如下:

# 訂閱模式
psubscribe pattern
# 退訂模式
punsubscribe pattern

那麼我們如何用釋出/訂閱來實現訊息佇列?我們可以使用模式訂閱的功能,利用一個消費者"queue_"來訂閱所有以"queue__"開頭的訊息佇列。如下圖:

但是釋出訂閱模式也存在以下缺點:

  • 無法持久化儲存訊息
  • 釋出訂閱模式是“先發後忘”的工作模式,若有訂閱者離線,重連後不能消費之前的歷史訊息
  • 不支援消費者確認機制,穩定性無法得到保證

2.3 基於Stream 實現訊息佇列

然而在 Redis 5.0 之後新增了 Stream 型別,它提供了豐富的訊息佇列操作命令:

  • XADD:插入訊息,保證 MQ 有序,可以自動生成全域性唯一 ID

    # mqstream 為訊息佇列,訊息的鍵是 repo 值為5
    # * 表示自動生成一個全域性唯一ID
    XADD mqstream * repo 5
    
  • XREAD:用於讀取訊息,可以按 ID 讀取資料,保證MQ對重複訊息的處理

    # 從 1599203861727-0 起讀取後續的所有訊息
    XREAD BLOCK 100 STREAMS mqstream 1599203861727-0
    

    XREAD 後的block 配置項,類似於 brpop 命令的阻塞讀取操作,後面的 100 的單位是毫秒,表示如果沒有訊息到來,XREAD 將阻塞 100 毫秒。

  • XREADGROUP:按消費組形式讀取訊息;

    # 建立名為 group1 的消費組,其消費佇列是 mqstream
    XGROUP create mqstream group1 0
    # 讓 group1 消費組裡的消費者 consumer1 從 mqstream 中讀取所有訊息
    # 命令最後的引數 ">" 表示從第一條尚未被消費的訊息開始讀取
    XREADGROUP group group1 consumer1 streams mqstream >
    

    使用消費組的目的是讓組內的多個消費者共同分擔讀取訊息,通常會讓每個消費者讀取部分訊息,從而實現訊息讀取負載在多個消費者間是均衡分佈的。

  • XPENDING 和 XACK:XPENDING 命令可以用來查詢每個消費組內所有消費者已讀取但尚未確認的訊息(保證消費者在發生故障或當機再次重啟後,仍然可以讀取未處理完的訊息),而 XACK 命令用於向訊息佇列確認訊息處理已完成。

2.4 總結

List 和 Streams 實現訊息佇列的特點和區別:

關於 Redis 是否適合做訊息佇列,引用一下蔣德鈞老師的看法:

Redis 是一個非常輕量級的鍵值資料庫,部署一個 Redis 例項就是啟動一個程式,部署 Redis 叢集,也就是部署多個 Redis 例項。而 Kafka、RabbitMQ 部署時,涉及額外的元件,例如 Kafka 的執行就需要再部署 ZooKeeper。相比 Redis 來說,Kafka 和 RabbitMQ 一般被認為是重量級的訊息佇列。所以,關於是否用 Redis 做訊息佇列的問題,不能一概而論,我們需要考慮業務層面的資料體量,以及對效能、可靠性、可擴充套件性的需求。如果分散式系統中的元件訊息通訊量不大,那麼,Redis 只需要使用有限的記憶體空間就能滿足訊息儲存的需求,而且,Redis 的高效能特效能支援快速的訊息讀寫,不失為訊息佇列的一個好的解決方案。

參考資料

https://zhuanlan.zhihu.com/p/86812691

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1775

https://time.geekbang.org/column/article/284291

https://www.cnblogs.com/weifeng1463/p/12889300.html

https://pdai.tech/md/db/nosql-redis/db-redis-x-pub-sub.html

《Redis 設計與實現》

《Redis 開發與運維》

相關文章