硬核 | Redis Pub/Sub 發(fā)布訂閱與宅男有什么關(guān)系?
今天不聊小姐姐,深入了解下 「Redis 發(fā)布/訂閱機(jī)制」的原理與實(shí)戰(zhàn)運(yùn)用。
Redis 通過 SUBSCRIBE,UNSUBSCRIBE和PUBLISH 實(shí)現(xiàn)發(fā)布訂閱消息傳遞模式,Redis 提供了兩種模式實(shí)現(xiàn),分別是「發(fā)布/訂閱到頻道」和「發(fā)布\訂閱到模式」。
Redis 發(fā)布訂閱簡介
Redis 發(fā)布訂閱(Pus/Sub)是一種消息通信模式:發(fā)送者通過 PUBLISH發(fā)布消息,訂閱者通過 SUBSCRIBE 訂閱接收消息或通過UNSUBSCRIBE 取消訂閱。
主要包含三個(gè)部分組成:「發(fā)布者」、「訂閱者」、「Channel」。
發(fā)布者和訂閱者屬于客戶端,Channel 是 Redis 服務(wù)端,發(fā)布者將消息發(fā)布到頻道,訂閱這個(gè)頻道的訂閱者則收到消息。
如下圖所示,三個(gè)「訂閱者」訂閱「ChannelA」頻道:

訂閱
這時(shí)候,小組長往「ChannelA」發(fā)布消息,這個(gè)消息的訂閱者就會(huì)收到消息「關(guān)注碼哥字節(jié),提升技術(shù)」:

發(fā)布/訂閱
Pub/Sub 實(shí)戰(zhàn)
廢話不多說,知道基本概念以后,學(xué)習(xí)一個(gè)技術(shù)第一步先把它跑起來,接著才是探索原理,從而達(dá)到「知其然,知其所以然」的境界 。
一共有兩種模式實(shí)現(xiàn)「發(fā)布\訂閱」:
- 使用頻道(Channel)的發(fā)布訂閱;
- 使用模式(Pattern)的發(fā)布訂閱。
需要注意的是,發(fā)布訂閱機(jī)制與 db 空間無關(guān),比如在 db 10 發(fā)布, db0 的訂閱者也會(huì)收到消息。
通過頻道(Channel)實(shí)現(xiàn)
三步走:
- 訂閱者訂閱頻道;
- 發(fā)布者向「頻道」發(fā)布消息;
- 所有訂閱「頻道」的訂閱者收到消息。
訂閱者訂閱頻道
使用 SUBSCRIBE channel [channel ...]訂閱一個(gè)或者多個(gè)頻道,O(n) 時(shí)間復(fù)雜度,n = 訂閱的 Channel 數(shù)量。
SUBSCRIBE develop
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 消息類型
2) "develop" // 頻道
3) (integer) 1 // 消息內(nèi)容
執(zhí)行該指令后,客戶端進(jìn)入訂閱狀態(tài),訂閱者只能使用subscribe、unsubscribe、psubscribe和punsubscribe這四個(gè)屬于"發(fā)布/訂閱" 的指令。
客戶端「肖菜雞」訂閱了 「develop」頻道接受組長的消息,消息響應(yīng)體分別表示:
- 消息類型:subscribe、message、unsubscribe
- 頻道
- 消息內(nèi)容:隨著消息類型不同代表不同含義。
進(jìn)入訂閱后的客戶端可以收到 3 種類型的消息回復(fù):
- subscribe:訂閱成功的反饋消息,第二個(gè)值是訂閱成功的頻道名稱,第三個(gè)是當(dāng)前客戶端訂閱的頻道數(shù)量。
- message:客戶端接收到消息,第二個(gè)值表示產(chǎn)生消息的頻道名稱,第三個(gè)值是消息的內(nèi)容。
- unsubscribe:表示成功取消訂閱某個(gè)頻道。第二個(gè)值是對(duì)應(yīng)的頻道名稱,第三個(gè)值是當(dāng)前客戶端訂閱的頻道數(shù)量,當(dāng)此值為 0 時(shí)客戶端會(huì)退出訂閱狀態(tài),之后就可以執(zhí)行其他非"發(fā)布/訂閱"模式的命令了。
發(fā)布者發(fā)布消息
小組長使用 PUBLISH channel message 向指定 「develop」頻道發(fā)布消息。
PUBLISH develop 'do job'
(integer) 1
需要注意的是,發(fā)布的消息并不會(huì)持久化,消息發(fā)布之后還有新「開發(fā)」靚仔訂閱的話,只能接收后續(xù)發(fā)布到該頻道的消息。
好一個(gè)「不問過往,只爭當(dāng)下」。
訂閱者接收消息
關(guān)注了「develop」頻道的訂閱者將會(huì)收到「do job」消息。
// 訂閱 develop 頻道
SUBSCRIBE develop
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 訂閱頻道成功
2) "develop" // 頻道
3) (integer) 1
// 當(dāng)發(fā)布者發(fā)布消息,訂閱者讀取到的消息如下
1) "message" // 接受到消息
2) "develop" // 頻道名稱
3) "do job" // 消息內(nèi)容
退訂頻道
訂閱的反向操作,「65 哥」天天在朋友圈秀恩愛,受不了了,取消訂閱他的朋友圈。
使用 UNSUBSCRIBE 命令可以退訂指定的「模式」不會(huì)影響通過 `subscribe 命令訂閱的頻道。
同樣 unsubscribe命令也不會(huì)影響通過psubscribe命令訂閱的規(guī)則。
通過模式(Pattern)實(shí)現(xiàn)
接下來看另一種方式實(shí)現(xiàn)發(fā)布訂閱,如下圖表示當(dāng)「匹配模式」與這個(gè)頻道匹配的話,當(dāng)消息向頻道發(fā)布消息,該消息還會(huì)發(fā)布到與這個(gè)頻道匹配的「模式」上,訂閱這個(gè)模式的客戶端也會(huì)收到消息。
smile.girl.* 模式表示「你微笑時(shí)好美」pattern,與這個(gè)模式匹配的兩個(gè)頻道是 smile.girls.Tina、smile.girls.maggi ,分別表示喜歡「微笑的 Tina」 和喜歡「微笑的 maggi」的粉絲。
如下圖:

模式匹配
現(xiàn)在 Tina 發(fā)布動(dòng)態(tài)將消息發(fā)送到 smile.girls.Tina頻道的時(shí)候,除了訂閱了 smile.girls.Tina 這個(gè)頻道的粉絲收到消息以外,這 個(gè)消息還會(huì)發(fā)送給訂閱 smile.girl.* 模式的粉絲(因?yàn)轭l道與模式匹配)。
這些粉絲比較貪心,所有「微笑時(shí)好美的 girls」都關(guān)注了,LSP~~,碼哥可不是這樣的人。

模式匹配發(fā)布
使用匹配模式,用 PUBLISH 將消息發(fā)布到訂閱 smile.girls.Tina 客戶端之外,還會(huì)將該「頻道」與「pub/sub pattern」中的模式進(jìn)行對(duì)比,如果 Channel 與某個(gè)模式匹配的話,也將這個(gè)消息發(fā)布到訂閱這個(gè)模式的客戶端。
訂閱模式
訂閱模式的指令是PSUBSCRIBE,如下表示 LSP 訂閱「smile.girl.*」模式:
PSUBSCRIBE smile.girls.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe" // 消息類型
2) "smile.girls.*"// 模式
3) (integer) 1 //訂閱數(shù)
對(duì)應(yīng)的反向取消模式訂閱的指令是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 發(fā)布消息,關(guān)注「smile.girls.Tina」的粉絲和訂閱了與該頻道匹配的「smile.girls.*」模式的粉絲收到消息。
關(guān)注 「smile.girls.*」模式的粉絲收到消息:
PSUBSCRIBE smile.girls.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "smile.girls.*"
3) (integer) 1
//進(jìn)入訂閱狀態(tài),接收到消息
1) "pmessage" 消息類型
2) "smile.girls.*"
3) "smile.girls.Tina"
4) "love u" // 消息內(nèi)容
關(guān)注「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"
需要注意的是,如果一個(gè)客戶端訂閱了與模式匹配的模式和頻道,那么客戶端會(huì)收到多次消息。
比如,65 哥 訂閱了「smile.girls.Tina」頻道和「smile.girls.*」模式,那么當(dāng) Tina 發(fā)布動(dòng)態(tài)到頻道的時(shí)候,65 哥會(huì)收到兩條票消息,一條消息類型是message,一條類型是pmessage。
Redisson 與 SpringBoot 實(shí)戰(zhàn)
官方文檔:https://github.com/redisson/redisson/wiki/6.-distributed-objects/#67-topic
生產(chǎn)者代碼
/**
* 發(fā)布消息到 Topic
* @param message 消息
* @return 接收消息的客戶端數(shù)量
*/
public long sendMessage(String message) {
RTopic topic = redissonClient.getTopic(CHANNEL);
long publish = topic.publish(message);
log.info("生產(chǎn)者發(fā)送消息成功,msg = {}", message);
return publish;
}
消費(fèi)者代碼
public void onMessage() {
// in other thread or JVM
RTopic topic = redissonClient.getTopic(CHANNEL);
topic.addListener(String.class, (channel, msg) -> {
log.info("channel: {} 收到消息 {}.", channel, msg);
});
}
需要注意的是,發(fā)布消息與監(jiān)聽消息要運(yùn)行在不同的 JVM,如果使用同一個(gè) redissonClient 發(fā)布的話,不會(huì)監(jiān)聽到自己的消息。
原理分析
我們通過上文知道了發(fā)布訂閱的概念,一共兩種模式實(shí)現(xiàn)發(fā)布訂閱。并且運(yùn)用原生指令和 Redisson 進(jìn)行實(shí)戰(zhàn)。
接下來,我們要深入理解 Redis 如何實(shí)現(xiàn)發(fā)布訂閱機(jī)制,做到知其然知其所以然。
頻道(Channel)的發(fā)布/訂閱如何實(shí)現(xiàn)的?
65 哥,如果是你會(huì)使用什么數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)基于頻道來定位對(duì)應(yīng)客戶端?
碼哥,我覺得可以字典來實(shí)現(xiàn),字典的 key 對(duì)應(yīng)被訂閱的頻道,而字典的值可以使用一個(gè)鏈表,鏈表里面保存著訂閱這個(gè)頻道的所有客戶端。
數(shù)據(jù)結(jié)構(gòu)
聰明,Redis 使用 redis.h中有一個(gè) redisServer 結(jié)構(gòu)體維護(hù)每個(gè)服務(wù)器進(jìn)程表示服務(wù)器狀態(tài),pubsub_channels 屬性是一個(gè)字典,用于保存訂閱頻道的信息。
struct redisServer {
...
/* Pubsub */
dict *pubsub_channels;
...
}
如下圖所示,「碼哥」、「靚仔」訂閱了「redis-channel」,「宅男」「LSP」訂閱了「枝~藤¥由*香-里」:

頻道訂閱發(fā)布原理
發(fā)送消息到頻道
生產(chǎn)者調(diào)用 PUBLISH channel messsage 發(fā)送消息,程序先根據(jù) channel 從 pubsub_channels 定位到字典的 key 所在的桶,接著把消息發(fā)送給這個(gè) key 對(duì)應(yīng)的 value 鏈表的所有客戶端。
退訂頻道
UNSUBSCRIBE命令可以退訂指定的頻道:對(duì)于字典操作來說,根據(jù) key 找到關(guān)注鏈表,遍歷鏈表,刪除這個(gè)客戶端,這樣消息就不會(huì)發(fā)送給這個(gè)客戶端了。
模式(Pattern)的發(fā)布/訂閱如何實(shí)現(xiàn)的?
接下來,我們繼續(xù)看基于模式實(shí)現(xiàn)的發(fā)布訂閱原理……
當(dāng)使用 PUBLISH發(fā)布消息到某個(gè)頻道的時(shí)候,不僅訂閱這個(gè)頻道的所有客戶端會(huì)收到消息,與這個(gè)模式匹配的客戶端也會(huì)收到消息。
源碼在 server.h 文件中的redisServer.pubsub_patterns 屬性定義。
struct redisServer {
...
/* A dict of pubsub_patterns */
dict *pubsub_patterns;
...
}
也是 dict 字典類型, key 對(duì)應(yīng)「pattern」模式,value 是一個(gè) 鏈表類型的結(jié)構(gòu):list *clients里面包含匹配這個(gè)模式的客戶端列表。
當(dāng)執(zhí)行 PSUBSCRIBE smile.girls.*命令的時(shí)候,會(huì)執(zhí)行pubsubSubscribePattern方法。
在這里我分享下如何定位關(guān)鍵源碼,發(fā)布訂閱我們根據(jù)經(jīng)驗(yàn)搜索pubsub便能檢索到 pubsub.c:

pubsub.c
碼哥使用 CLion 調(diào)試的 Redis 源碼,跟我們 Java 開發(fā)用的 IDEA 出自于一家,所以快捷鍵都是一樣的,接著使用 Command + F12 彈出方法搜索,找到 pubsubSubscribePattern 訂閱模式的方法。
方法參數(shù)別分表示關(guān)注該模式的客戶端 client *c,和客戶端想要關(guān)注的 *pattern,方法主要邏輯如下:
- listSearchKey(c->pubsub_patterns,pattern):根據(jù) pattern 從 redisServer.pubsub_patterns 字典查找是否已經(jīng)存在該模式的 key,存在則調(diào)用addReplyPubsubPatSubscribed 通知客戶端已經(jīng)訂閱過了,否則繼續(xù)執(zhí)行以下邏輯。
- dictFind(server.pubsub_patterns,pattern):根據(jù)模式pattern從字典server.pubsub_patterns找到 dictEntry 哈希桶,為空就調(diào)用listCreate()創(chuàng)建客戶端鏈表list *clients,并放到字典中,key = pattern,value = list *clients 鏈表。
哈希桶不為空,那么把當(dāng)前客戶端client *c 添加到list *clients鏈表尾節(jié)點(diǎn)。

訂閱模式源碼
所以模式實(shí)現(xiàn)的發(fā)布訂閱也是通過字典來保存模式與客戶端的關(guān)系,如下圖所示:

基于模式實(shí)現(xiàn)的發(fā)布訂閱原理
當(dāng)使用 PUBLISH 發(fā)布消息的時(shí)候,除了發(fā)布到訂閱channel的客戶端以外,還會(huì)將該 channel 與 pubsub_patterns 字典中查找匹配模式 key 對(duì)應(yīng)的 value 中的客戶端鏈表,并執(zhí)行消息發(fā)送。
退訂模式
使用 PUNSUBSCRIBE命令可以退訂指定的模式, 這個(gè)命令執(zhí)行的是訂閱模式的反操作:根據(jù)模式從 pubsub_patterns字典中找到客戶端鏈表,遍歷鏈表將當(dāng)前客戶端刪除。
總結(jié)
Redis 發(fā)布訂閱功能,主要通過如下命令實(shí)現(xiàn):
- subscribe channel [channel ...]:訂閱一個(gè)或者多個(gè)頻道;
- unsubscribe channel 退訂指定頻道;
- publish channel message 向指定頻道發(fā)送消息;
- psubscribe pattern 訂閱指定模式;
- punsubscribe pattern 退訂指定模式。
Pub/Sub 與數(shù)據(jù)庫無關(guān),比如在 DB0 上發(fā)布, DB1的訂閱者也將接收到。
基于頻道實(shí)現(xiàn)的發(fā)布訂閱信息是由服務(wù)器進(jìn)程的 redisServer.pubsub_channels 字典保存,key = 被訂閱的頻道,value 是訂閱頻道的所有客戶端鏈表。
當(dāng)消息發(fā)布到頻道的時(shí)候,遍歷字典獲取所有客戶端并把消息發(fā)送到頻道的客戶端。
基于模式實(shí)現(xiàn)的發(fā)布訂閱的信息保存在字典 pubsub_patterns中,key = pattern,value 是客戶端鏈表。
當(dāng)消息發(fā)布到頻道的時(shí)候,除了訂閱該頻道的客戶端收到消息以外,所有訂閱了與頻道匹配的模式的客戶端也會(huì)收到消息。
使用場景
說了這么多,Redis 發(fā)布訂閱能在什么場景發(fā)揮作用呢?
哨兵間通信
哨兵集群中,每個(gè)哨兵節(jié)點(diǎn)利用 Pub/Sub 發(fā)布訂閱實(shí)現(xiàn)哨兵之間的相互發(fā)現(xiàn)彼此和找到 Slave。
哨兵與 Master 建立通信后,利用 master 提供發(fā)布/訂閱機(jī)制在__sentinel__:hello發(fā)布自己的信息,比如身高體重、是否單身、IP、端口……,同時(shí)訂閱這個(gè)頻道來獲取其他哨兵的信息,就這樣實(shí)現(xiàn)哨兵間通信。
消息隊(duì)列
之前「碼哥」跟大家分享過如何利用 Redis List 與 Stream 實(shí)現(xiàn)消息隊(duì)列。
我們也可以利用 Redis 發(fā)布訂閱實(shí)現(xiàn)輕量級(jí)簡單的 MQ 功能,實(shí)現(xiàn)上下游解耦,需要注意點(diǎn)是 Redis 發(fā)布訂閱的消息不會(huì)被持久化,所以新訂閱的客戶端將收不到歷史消息。
也不支持 ACK 機(jī)制,所以當(dāng)前業(yè)務(wù)不能容忍這些缺點(diǎn),那就使用專業(yè)的消息隊(duì)列,如果能容忍那就能享受 Redis 唯快不破的優(yōu)勢(shì)。






























