Redis 釋出訂閱模式:原理拆解並實現一個訊息佇列

碼哥位元組發表於2023-01-12

“65 哥,如果你交了個漂亮小姐姐做女朋友,你會透過什麼方式將這個訊息廣而告之給你的微信好友?“

“那不得拍點女朋友的美照 + 親密照弄一個九宮格圖文訊息在朋友圈釋出大肆宣傳,暴擊單身狗。”

像這種 65 哥透過朋友圈釋出訊息,關注 65 哥的好友能收到通知的場景叫做「釋出/訂閱機制」。

今天不聊小姐姐,深入瞭解下 「Redis 釋出/訂閱機制」。的原理與實戰運用。

Redis 透過 SUBSCRIBEUNSUBSCRIBEPUBLISH 實現釋出訂閱訊息傳遞模式,Redis 提供了兩種模式實現,分別是「釋出/訂閱到頻道」和「釋出\訂閱到模式」。

Redis 釋出訂閱簡介

Redis 釋出訂閱(Pus/Sub)是一種訊息通訊模式:傳送者透過 PUBLISH釋出訊息,訂閱者透過 SUBSCRIBE 訂閱接收訊息或透過UNSUBSCRIBE 取消訂閱。

主要包含三個部分組成:「釋出者」、「訂閱者」、「Channel」。

釋出者和訂閱者屬於客戶端,Channel 是 Redis 服務端,釋出者將訊息釋出到頻道,訂閱這個頻道的訂閱者則收到訊息。

如下圖所示,三個「訂閱者」訂閱「ChannelA」頻道:

訂閱

這時候,小組長往「ChannelA」釋出訊息,這個訊息的訂閱者就會收到訊息「關注碼哥位元組,提升技術」:

釋出/訂閱

Pub/Sub 實戰

廢話不多說,知道基本概念以後,學習一個技術第一步先把它跑起來,接著才是探索原理,從而達到「知其然,知其所以然」的境界 。

一共有兩種模式實現「釋出\訂閱」:

  • 使用頻道(Channel)的釋出訂閱;
  • 使用模式(Pattern)的釋出訂閱。

需要注意的是,釋出訂閱機制與 db 空間無關,比如在 db 10 釋出, db0 的訂閱者也會收到訊息。

透過頻道(Channel)實現

三步走:

  1. 訂閱者訂閱頻道;
  2. 釋出者向「頻道」釋出訊息;
  3. 所有訂閱「頻道」的訂閱者收到訊息。

訂閱者訂閱頻道

使用 SUBSCRIBE channel [channel ...]訂閱一個或者多個頻道,O(n) 時間複雜度,n = 訂閱的 Channel 數量。

SUBSCRIBE develop
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 訊息型別
2) "develop" // 頻道
3) (integer) 1 // 訊息內容

執行該指令後,客戶端進入訂閱狀態,訂閱者只能使用subscribeunsubscribepsubscribepunsubscribe這四個屬於"釋出/訂閱" 的指令。

客戶端「肖菜雞」訂閱了 「develop」頻道接受組長的訊息,訊息響應體分別表示:

  • 訊息型別:subscribe、message、unsubscribe
  • 頻道
  • 訊息內容:隨著訊息型別不同代表不同含義。

進入訂閱後的客戶端可以收到 3 種型別的訊息回覆:

  1. subscribe:訂閱成功的反饋訊息,第二個值是訂閱成功的頻道名稱,第三個是當前客戶端訂閱的頻道數量。
  2. message:客戶端接收到訊息,第二個值表示產生訊息的頻道名稱,第三個值是訊息的內容。
  3. unsubscribe:表示成功取消訂閱某個頻道。第二個值是對應的頻道名稱,第三個值是當前客戶端訂閱的頻道數量,當此值為 0 時客戶端會退出訂閱狀態,之後就可以執行其他非"釋出/訂閱"模式的命令了。

釋出者釋出訊息

小組長使用 PUBLISH channel message 向指定 「develop」頻道釋出訊息。

PUBLISH develop 'do job'
(integer) 1

需要注意的是,釋出的訊息並不會持久化,訊息釋出之後還有新「開發」靚仔訂閱的話,只能接收後續釋出到該頻道的訊息。

好一個「不問過往,只爭當下」。

訂閱者接受訊息

關注了「develop」頻道的訂閱者將會收到「do job」訊息。

// 訂閱 develop 頻道
SUBSCRIBE develop
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 訂閱頻道成功
2) "develop" // 頻道
3) (integer) 1
// 當釋出者釋出訊息,訂閱者讀取到的訊息如下
1) "message" // 接受到訊息
2) "develop" // 頻道名稱
3) "do job" // 訊息內容

退訂頻道

訂閱的反向操作,「65 哥」天天在朋友圈秀恩愛,受不了了,取消訂閱他的朋友圈。

使用 UNSUBSCRIBE 命令可以退訂指定的「模式」不會影響透過 `subscribe 命令訂閱的頻道。

同樣 unsubscribe命令也不會影響透過psubscribe命令訂閱的規則。

透過模式(Pattern)實現

接下來看另一種方式實現釋出訂閱,如下圖表示當「匹配模式」與這個頻道匹配的話,當訊息向頻道釋出訊息,該訊息還會發布到與這個頻道匹配的「模式」上,訂閱這個模式的客戶端也會收到訊息。

smile.girl.* 模式表示「你微笑時好美」pattern,與這個模式匹配的兩個頻道是 smile.girls.Tinasmile.girls.maggi ,分別表示喜歡「微笑的 Tina」 和喜歡「微笑的 maggi」的粉絲。

如下圖:

模式匹配

現在 Tina 釋出動態將訊息傳送到 smile.girls.Tina頻道的時候,除了訂閱了 smile.girls.Tina 這個頻道的粉絲收到訊息以外,這 個訊息還會傳送給訂閱 smile.girl.* 模式的粉絲(因為頻道與模式匹配)。

這些粉絲比較貪心,所有「微笑時好美的 girls」都關注了,LSP~~,碼哥可不是這樣的人。

模式匹配發布

使用匹配模式,用 PUBLISH 將訊息釋出到訂閱 smile.girls.Tina 客戶端之外,還會將該「頻道」與「pub/sub pattern」中的模式進行對比,如果 Channel 與某個模式匹配的話,也將這個訊息釋出到訂閱這個模式的客戶端。

訂閱模式

訂閱模式的指令是PSUBSCRIBE,如下表示 LSP 訂閱「smile.girl.*」模式:

PSUBSCRIBE smile.girls.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe" // 訊息型別
2) "smile.girls.*"// 模式
3) (integer) 1 //訂閱數

對應的反向取消模式訂閱的指令是PUNSUBSCRIBE smile.girl.*

訂閱 「smile.girls.Tina」頻道

SUBSCRIBE smile.girls.Tina
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "smile.girls.Tina"
3) (integer) 1

訂閱「smile.girls.maggi」頻道

SUBSCRIBE smile.girls.maggi
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "smile.girls.maggi"
3) (integer) 1

Tina 釋出訊息,關注「smile.girls.Tina」的粉絲和訂閱了與該頻道匹配的「smile.girls.*」模式的粉絲收到訊息。

關注 「smile.girls.*」模式的粉絲收到訊息

PSUBSCRIBE smile.girls.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "smile.girls.*"
3) (integer) 1
//進入訂閱狀態,接收到訊息
1) "pmessage" 訊息型別
2) "smile.girls.*"
3) "smile.girls.Tina"
4) "love u" // 訊息內容

關注「smile.girls.Tina」的粉絲收到訊息

127.0.0.1:6379> SUBSCRIBE smile.girls.Tina
Reading messages... (press Ctrl-C to quit)
// 訂閱成功
1) "subscribe"
2) "smile.girls.Tina"
3) (integer) 1
// 接收訊息
1) "message"
2) "smile.girls.Tina"
3) "love u"

需要注意的是,如果一個客戶端訂閱了與模式匹配的模式和頻道,那麼客戶端會收到多次訊息。

比如,65 哥 訂閱了「smile.girls.Tina」頻道和「smile.girls.*」模式,那麼當 Tina 釋出動態到頻道的時候,65 哥會收到兩條票訊息,一條訊息型別是message,一條型別是pmessage

Redisson 與 SpringBoot 實戰

官方文件:https://github.com/redisson/r...

生產者程式碼

/**
 * 釋出訊息到 Topic
 * @param message 訊息
 * @return 接收訊息的客戶端數量
 */
public long sendMessage(String message) {
    RTopic topic = redissonClient.getTopic(CHANNEL);
    long publish = topic.publish(message);
    log.info("生產者傳送訊息成功,msg = {}", message);
    return publish;
}

消費者程式碼

public void onMessage() {
  // in other thread or JVM
  RTopic topic = redissonClient.getTopic(CHANNEL);
  topic.addListener(String.class, (channel, msg) -> {
    log.info("channel: {} 收到訊息 {}.",  channel, msg);
  });
}

需要注意的是,釋出訊息與監聽訊息要執行在不同的 JVM,如果使用同一個 redissonClient 釋出的話,不會監聽到自己的訊息。

原理分析

我們透過上文知道了釋出訂閱的概念,一共兩種模式實現釋出訂閱。並且運用原生指令和 Redisson 進行實戰。

接下來,我們要深入理解 Redis 如何實現釋出訂閱機制,做到知其然知其所以然。

頻道(Channel)的釋出/訂閱如何實現的?

65 哥,如果是你會使用什麼資料結構來實現基於頻道來定位對應客戶端?

碼哥,我覺得可以字典來實現,字典的 key 對應被訂閱的頻道,而字典的值可以使用一個連結串列,連結串列裡面儲存著訂閱這個頻道的所有客戶端。

資料結構

聰明,Redis 使用 redis.h中有一個 redisServer 結構體維護每個伺服器程式表示伺服器狀態,pubsub_channels 屬性是一個字典,用於儲存訂閱頻道的資訊。

struct redisServer {
  ...
  /* Pubsub */
   dict *pubsub_channels;
  ...
}

如下圖所示,「碼哥」、「靚仔」訂閱了「redis-channel」,「宅男」「LSP」訂閱了「枝~藤¥由*香-裡」:

頻道訂閱釋出原理

傳送訊息到頻道

生產者呼叫 PUBLISH channel messsage 傳送訊息,程式先根據 channel 從 pubsub_channels 定位到字典的 key 所在的桶,接著把訊息傳送給這個 key 對應的 value 連結串列的所有客戶端。

退訂頻道

UNSUBSCRIBE命令可以退訂指定的頻道:丟與字典操作來說,根據 key 找到關注連結串列,遍歷連結串列,刪除這個客戶端,這樣訊息就不會傳送給這個客戶端了。

模式(Pattern)的釋出/訂閱如何實現的?

接下來,我們繼續看基於模式實現的釋出訂閱原理……

當使用 PUBLISH釋出訊息到某個頻道的時候,不僅訂閱這個頻道的所有客戶端會收到訊息,與這個模式匹配的客戶端也會收到訊息。

原始碼在 server.h 檔案中的redisServer.pubsub_patterns 屬性定義。

struct redisServer {
  ...
  /* A dict of pubsub_patterns */
    dict *pubsub_patterns;
  ...
}

也是 dict 字典型別, key 對應「pattern」模式,value 是一個 連結串列型別的結構: list *clients裡面包含匹配個模式的客戶端列表。

當執行 PSUBSCRIBE smile.girls.*命令的時候,會執行pubsubSubscribePattern方法。

在這裡我分享下如何定位關鍵原始碼,釋出訂閱我們根據經驗搜尋pubsub便能檢索到 pubsub.c

pubsub.c

碼哥使用 CLion 除錯的 Redis 原始碼,跟我們 Java 開發用的 IDEA 出自於一家,所以快捷鍵都是一樣的,接著使用 Command + F12 彈出方法搜尋,找到 pubsubSubscribePattern 訂閱模式的方法。

方法引數別分表示關注該模式的客戶端 client c,和客戶端想要關注的 pattern,方法主要邏輯如下:

  1. listSearchKey(c->pubsub_patterns,pattern):根據 pattern 從 redisServer.pubsub_patterns 字典查詢是否已經存在該模式的 key,存在則呼叫addReplyPubsubPatSubscribed 通知客戶端已經訂閱過了,否則繼續執行以下邏輯。
  2. dictFind(server.pubsub_patterns,pattern):根據模式 pattern從字典 server.pubsub_patterns找到 dictEntry 雜湊桶,為空就呼叫 listCreate()建立客戶端連結串列 list *clients,並放到字典中,key = pattern,value = list *clients 連結串列。
  3. 雜湊桶不為空,那麼把當前客戶端 client *c 新增到 list *clients連結串列尾節點。

訂閱模式原始碼

所以模式實現的釋出訂閱也是透過字典來儲存模式與客戶端的關係,如下圖所示:

基於模式實現的釋出訂閱原理

當使用 PUBLISH 釋出訊息的時候,除了釋出到訂閱channel的客戶端以外,還會將該 channel 與 pubsub_patterns 字典中查詢匹配模式 key 對應的 value 中的客戶端連結串列,並執行訊息傳送。

退訂模式

使用 PUNSUBSCRIBE命令可以退訂指定的模式, 這個命令執行的是訂閱模式的反操作:根據模式從 pubsub_patterns字典中找到客戶端連結串列,遍歷連結串列將當前客戶端刪除。

總結

Redis 釋出訂閱功能,主要透過如下命令實現:

  • subscribe channel [channel ...]:訂閱一個或者多個頻道;
  • unsubscribe channel 退訂指定頻道;
  • publish channel message 向指定頻道傳送訊息;
  • psubscribe pattern 訂閱指定模式;
  • punsubscribe pattern 退訂指定模式。

Pub/Sub 與資料庫無關,比如在 DB0 上釋出, DB1的訂閱者也將接收到。

基於頻道實現的釋出訂閱資訊是由伺服器程式的 redisServer.pubsub_channels 字典儲存,key = 被訂閱的頻道,value 是訂閱頻道的所有客戶端連結串列。

當訊息釋出到頻道的時候,遍歷字典獲取所有客戶端並把訊息傳送到頻道的客戶端。

基於模式實現的釋出訂閱的資訊儲存在字典 pubsub_patterns中,key = pattern,value 是客戶端連結串列。

當訊息釋出到頻道的時候,除了訂閱該頻道的客戶端收到訊息以外,所有訂閱了與頻道匹配的模式的客戶端也會收到訊息。

使用場景

說了這麼多,Redis 釋出訂閱能在什麼場景發揮作用呢?

哨兵間通訊

哨兵叢集中,每個哨兵節點利用 Pub/Sub 釋出訂閱實現哨兵之間的相互發現彼此和找到 Slave,詳情點選 ->《哨兵叢集原理那些事》

哨兵與 Master 建立通訊後,利用 master 提供釋出/訂閱機制在__sentinel__:hello釋出自己的資訊,比如身高體重、是否單身、IP、埠……,同時訂閱這個頻道來獲取其他哨兵的資訊,就這樣實現哨兵間通訊。

訊息佇列

之前「碼哥」跟大家分享過如何利用 Redis ListStream 實現訊息佇列

我們也可以利用 Redis 釋出訂閱實現輕量級簡單的 MQ 功能,實現上下游解耦,需要注意點是 Redis 釋出訂閱的訊息不會被持久化,所以新訂閱的客戶端將收不到歷史訊息。

也不支援 ACK 機制,所以當前業務不能容忍這些缺點,那就使用專業的訊息佇列,如果能容忍那就能享受 Redis 快帶來的優勢。

最後,可以在評論區叫我一聲「靚仔」麼?為了寫這個文章,碼哥看了好多微笑時好美的 girl 才寫出來,原創不易。

朋友們點贊、分享、收藏支援我吧。

參考資料

1.Redis 設計與實現

2.https://redisbook.readthedocs...

相關文章