Redis進階篇:發佈訂閱模式原理與運用
「65 哥,如果你交了個漂亮小姐姐做女朋友,你會通過什麼方式將這個消息廣而告之給你的微信好友?「
「那不得拍點女朋友的美照 + 親密照弄一個九宮格圖文消息在朋友圈發佈大肆宣傳,暴擊單身狗。」
像這種 65 哥通過朋友圈發佈消息,關注 65 哥的好友能收到通知的場景叫做「發佈/訂閱機制」。
今天不聊小姐姐,深入了解下 「Redis 發佈/訂閱機制」。的原理與實戰運用。
Redis 通過 SUBSCRIBE
,UNSUBSCRIBE
和PUBLISH
實現發佈訂閱消息傳遞模式,Redis 提供了兩種模式實現,分別是「發佈/訂閱到頻道」和「發佈\訂閱到模式」。
Redis 發佈訂閱簡介
Redis 發佈訂閱(Pus/Sub)是一種消息通信模式:發送者通過 PUBLISH
發佈消息,訂閱者通過 SUBSCRIBE
訂閱接收消息或通過UNSUBSCRIBE
取消訂閱。
主要包含三個部分組成:「發佈者」、「訂閱者」、「Channel」。
發佈者和訂閱者屬於客戶端,Channel 是 Redis 服務端,發佈者將消息發佈到頻道,訂閱這個頻道的訂閱者則收到消息。
如下圖所示,三個「訂閱者」訂閱「ChannelA」頻道:
這時候,小組長往「ChannelA」發佈消息,這個消息的訂閱者就會收到消息「關注碼哥位元組,提升技術」:
Pub/Sub 實戰
廢話不多說,知道基本概念以後,學習一個技術第一步先把它跑起來,接着才是探索原理,從而達到「知其然,知其所以然」的境界 。
一共有兩種模式實現「發佈\訂閱」:
- 使用頻道(Channel)的發佈訂閱;
- 使用模式(Pattern)的發佈訂閱。
需要注意的是,發佈訂閱機制與 db 空間無關,比如在 db 10 發佈, db0 的訂閱者也會收到消息。
通過頻道(Channel)實現
三步走:
- 訂閱者訂閱頻道;
- 發佈者向「頻道」發佈消息;
- 所有訂閱「頻道」的訂閱者收到消息。
訂閱者訂閱頻道
使用 SUBSCRIBE channel [channel ...]
訂閱一個或者多個頻道,O(n) 時間複雜度,n = 訂閱的 Channel 數量。
SUBSCRIBE develop
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 消息類型
2) "develop" // 頻道
3) (integer) 1 // 消息內容
執行該指令後,客戶端進入訂閱狀態,訂閱者只能使用subscribe
、unsubscribe
、psubscribe
和punsubscribe
這四個屬於”發佈/訂閱” 的指令。
客戶端「肖菜雞」訂閱了 「develop」頻道接受組長的消息,消息響應體分別表示:
- 消息類型:subscribe、message、unsubscribe
- 頻道
- 消息內容:隨着消息類型不同代表不同含義。
進入訂閱後的客戶端可以收到 3 種類型的消息回復:
- subscribe:訂閱成功的反饋消息,第二個值是訂閱成功的頻道名稱,第三個是當前客戶端訂閱的頻道數量。
- message:客戶端接收到消息,第二個值表示產生消息的頻道名稱,第三個值是消息的內容。
- 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.Tina
、smile.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 實戰
官方文檔://github.com/redisson/redisson/wiki/6.-distributed-objects/#67-topic
生產者代碼
/**
* 發佈消息到 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
:
碼哥使用 CLion 調試的 Redis 源碼,跟我們 Java 開發用的 IDEA 出自於一家,所以快捷鍵都是一樣的,接着使用 Command + F12
彈出方法搜索,找到 pubsubSubscribePattern
訂閱模式的方法。
方法參數別分表示關注該模式的客戶端 client *c,和客戶端想要關注的 *pattern,方法主要邏輯如下:
listSearchKey(c->pubsub_patterns,pattern)
:根據 pattern 從 redisServer.pubsub_patterns 字典查找是否已經存在該模式的 key,存在則調用addReplyPubsubPatSubscribed
通知客戶端已經訂閱過了,否則繼續執行以下邏輯。dictFind(server.pubsub_patterns,pattern)
:根據模式pattern
從字典server.pubsub_patterns
找到 dictEntry 哈希桶,為空就調用listCreate()
創建客戶端鏈表list *clients
,並放到字典中,key = pattern,value = list *clients 鏈表。- 哈希桶不為空,那麼把當前客戶端
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 List 與 Stream 實現消息隊列。
我們也可以利用 Redis 發佈訂閱實現輕量級簡單的 MQ 功能,實現上下游解耦,需要注意點是 Redis 發佈訂閱的消息不會被持久化,所以新訂閱的客戶端將收不到歷史消息。
也不支持 ACK 機制,所以當前業務不能容忍這些缺點,那就使用專業的消息隊列,如果能容忍那就能享受 Redis 快帶來的優勢。
最後,可以在評論區叫我一聲「靚仔」么?為了寫這個文章,碼哥看了好多微笑時好美的 girl 才寫出來,原創不易。
朋友們點贊、分享、收藏支持我吧。
參考資料
1.Redis 設計與實現
2.//redisbook.readthedocs.io/en/latest/feature/pubsub.html