# 訂閱與發布
[TOC=2,3]
Redis 通過 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 、 [SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html#subscribe "(in Redis 命令參考 v2.8)") 等命令實現了訂閱與發布模式,這個功能提供兩種信息機制,分別是訂閱/發布到頻道和訂閱/發布到模式,下文先討論訂閱/發布到頻道的實現,再討論訂閱/發布到模式的實現。
### 頻道的訂閱與信息發送
Redis 的 [SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html#subscribe "(in Redis 命令參考 v2.8)") 命令可以讓客戶端訂閱任意數量的頻道,每當有新信息發送到被訂閱的頻道時,信息就會被發送給所有訂閱指定頻道的客戶端。
作為例子,下圖展示了頻道 `channel1` ,以及訂閱這個頻道的三個客戶端 —— `client2` 、 `client5` 和 `client1` 之間的關系:
![digraph pubsub_relation { rankdir = BT; node [style = filled]; edge [style = bold]; channel1 [label = "channel1", fillcolor = "#A8E270"]; node [shape = box, fillcolor = "#95BBE3"]; client2 [label = "client2"]; client5 [label = "client5"]; client1 [label = "client1"]; client2 -> channel1 [label = "subscribe"]; client5 -> channel1 [label = "subscribe"]; client1 -> channel1 [label = "subscribe"];}](https://box.kancloud.cn/2015-09-13_55f4effd60979.svg)
當有新消息通過 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令發送給頻道 `channel1` 時,這個消息就會被發送給訂閱它的三個客戶端:
![digraph send_message_to_subscriber { node [style = filled]; edge [style = "dashed, bold"]; message [label = "PUBLISH channel1 message", shape = plaintext, fillcolor = "#FADCAD"]; message -> channel1 [color = "#B22222]"]; channel1 [label = "channel1", fillcolor = "#A8E270"]; node [shape = box]; client2 [label = "client2", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client1 [label = "client1", fillcolor = "#95BBE3"]; /* client2 -> channel1 [label = "subscribe"]; client5 -> channel1 [label = "subscribe"]; client1 -> channel1 [label = "subscribe"]; */ channel1 -> client2 [label = "message", color = "#B22222"]; channel1 -> client5 [label = "message", color = "#B22222"]; channel1 -> client1 [label = "message", color = "#B22222"];}](https://box.kancloud.cn/2015-09-13_55f4effd69833.svg)
在后面的內容中,我們將探討 [SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html#subscribe "(in Redis 命令參考 v2.8)") 和 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令的實現,以及這套訂閱與發布機制的運作原理。
### 訂閱頻道
每個 Redis 服務器進程都維持著一個表示服務器狀態的 `redis.h/redisServer` 結構,結構的 `pubsub_channels` 屬性是一個字典,這個字典就用于保存訂閱頻道的信息:
~~~
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};
~~~
其中,字典的鍵為正在被訂閱的頻道,而字典的值則是一個鏈表,鏈表中保存了所有訂閱這個頻道的客戶端。
比如說,在下圖展示的這個 `pubsub_channels` 示例中, `client2` 、 `client5` 和 `client1` 就訂閱了 `channel1` ,而其他頻道也分別被別的客戶端所訂閱:
![digraph pubsub { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys pubsub [label = "pubsub_channels |<channel1> channel1 |<channel2> channel2 |<channel3> channel3 | ... |<channelN> channelN", fillcolor = "#A8E270"]; // clients blocking for channel1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; null_1 [label = "NULL", shape = plaintext]; pubsub:channel1 -> client2; client2 -> client5; client5 -> client1; client1 -> null_1; // clients blocking for channel2 client7 [label = "client7", fillcolor = "#95BBE3"]; null_2 [label = "NULL", shape = plaintext]; pubsub:channel2 -> client7; client7 -> null_2; // channel client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; null_3 [label = "NULL", shape = plaintext]; pubsub:channel3 -> client3; client3 -> client4; client4 -> client6; client6 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd71e2f.svg)
當客戶端調用 [SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html#subscribe "(in Redis 命令參考 v2.8)") 命令時,程序就將客戶端和要訂閱的頻道在 `pubsub_channels` 字典中關聯起來。
舉個例子,如果客戶端 `client10086` 執行命令 `SUBSCRIBE channel1 channel2 channel3` ,那么前面展示的 `pubsub_channels` 將變成下面這個樣子:
![digraph new_subscribe { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys pubsub [label = "pubsub_channels |<channel1> channel1 |<channel2> channel2 |<channel3> channel3 | ... |<channelN> channelN", fillcolor = "#A8E270"]; // clients blocking for channel1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; client10086 [label = "client10086", fillcolor = "#FFC1C1"]; client10086_1 [label = "client10086", fillcolor = "#FFC1C1"]; client10086_2 [label = "client10086", fillcolor = "#FFC1C1"]; null_1 [label = "NULL", shape = plaintext]; null_2 [label = "NULL", shape = plaintext]; null_3 [label = "NULL", shape = plaintext]; pubsub:channel1 -> client2; client2 -> client5; client5 -> client1; client1 -> client10086; client10086 -> null_1; // clients blocking for channel2 client7 [label = "client7", fillcolor = "#95BBE3"]; pubsub:channel2 -> client7; client7 -> client10086_1; client10086_1 -> null_2; // channel client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; pubsub:channel3 -> client3; client3 -> client4; client4 -> client6; client6 -> client10086_2; client10086_2 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd7add8.svg)
[SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html#subscribe "(in Redis 命令參考 v2.8)") 命令的行為可以用偽代碼表示如下:
~~~
def SUBSCRIBE(client, channels):
# 遍歷所有輸入頻道
for channel in channels:
# 將客戶端添加到鏈表的末尾
redisServer.pubsub_channels[channel].append(client)
~~~
通過 `pubsub_channels` 字典,程序只要檢查某個頻道是否為字典的鍵,就可以知道該頻道是否正在被客戶端訂閱;只要取出某個鍵的值,就可以得到所有訂閱該頻道的客戶端的信息。
### 發送信息到頻道
了解了 `pubsub_channels` 字典的結構之后,解釋 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令的實現就非常簡單了:當調用 `PUBLISH channel message` 命令,程序首先根據 `channel` 定位到字典的鍵,然后將信息發送給字典值鏈表中的所有客戶端。
比如說,對于以下這個 `pubsub_channels` 實例,如果某個客戶端執行命令 `PUBLISH channel1 "hello moto"` ,那么 `client2` 、 `client5` 和 `client1` 三個客戶端都將接收到 `"hello moto"` 信息:
![digraph pubsub { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys pubsub [label = "pubsub_channels |<channel1> channel1 |<channel2> channel2 |<channel3> channel3 | ... |<channelN> channelN", fillcolor = "#A8E270"]; // clients blocking for channel1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; null_1 [label = "NULL", shape = plaintext]; pubsub:channel1 -> client2; client2 -> client5; client5 -> client1; client1 -> null_1; // clients blocking for channel2 client7 [label = "client7", fillcolor = "#95BBE3"]; null_2 [label = "NULL", shape = plaintext]; pubsub:channel2 -> client7; client7 -> null_2; // channel client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; null_3 [label = "NULL", shape = plaintext]; pubsub:channel3 -> client3; client3 -> client4; client4 -> client6; client6 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd84159.svg)
[PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令的實現可以用以下偽代碼來描述:
~~~
def PUBLISH(channel, message):
# 遍歷所有訂閱頻道 channel 的客戶端
for client in server.pubsub_channels[channel]:
# 將信息發送給它們
send_message(client, message)
~~~
### 退訂頻道
使用 [UNSUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/unsubscribe.html#unsubscribe "(in Redis 命令參考 v2.8)") 命令可以退訂指定的頻道,這個命令執行的是訂閱的反操作:它從 `pubsub_channels` 字典的給定頻道(鍵)中,刪除關于當前客戶端的信息,這樣被退訂頻道的信息就不會再發送給這個客戶端。
### 模式的訂閱與信息發送
當使用 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令發送信息到某個頻道時,不僅所有訂閱該頻道的客戶端會收到信息,如果有某個/某些模式和這個頻道匹配的話,那么所有訂閱這個/這些頻道的客戶端也同樣會收到信息。
下圖展示了一個帶有頻道和模式的例子,其中 `tweet.shop.*` 模式匹配了 `tweet.shop.kindle` 頻道和 `tweet.shop.ipad` 頻道,并且有不同的客戶端分別訂閱它們三個:
![digraph pattern_relation { rankdir = BT; node [style = filled]; edge [style = bold]; kindle [label = "tweet.shop.kindle", fillcolor = "#A8E270"]; ipad [label = "tweet.shop.ipad", fillcolor = "#A8E270"]; node [shape = octagon]; pattern [label = "tweet.shop.*"]; pattern -> kindle [label = "match"]; pattern -> ipad [label = "match"]; node [shape = box]; client123 [fillcolor = "#95BBE3"]; client256 [fillcolor = "#95BBE3"]; clientX [fillcolor = "#95BBE3"]; clientY [fillcolor = "#95BBE3"]; client3333 [fillcolor = "#95BBE3"]; client4444 [fillcolor = "#95BBE3"]; client5555 [fillcolor = "#95BBE3"]; client123 -> pattern [label = "subscribe"]; client256 -> pattern [label = "subscribe"]; clientX -> kindle [label = "subscribe"]; clientY -> kindle [label = "subscribe"]; client3333 -> ipad [label = "subscribe"]; client4444 -> ipad [label = "subscribe"]; client5555 -> ipad [label = "subscribe"];}](https://box.kancloud.cn/2015-09-13_55f4effd8d5d6.svg)
當有信息發送到 `tweet.shop.kindle` 頻道時,信息除了發送給 `clientX` 和 `clientY` 之外,還會發送給訂閱 `tweet.shop.*` 模式的 `client123` 和 `client256` :
![digraph send_message_to_pattern { node [style = filled]; edge [style = bold]; // tweet.shop.ipad ipad [label = "tweet.shop.ipad", fillcolor = "#A8E270"]; ipad -> pattern [label = "match", dir = back]; node [shape = box]; ipad -> client3333 [label = "subscribe", dir = back]; ipad -> client4444 [label = "subscribe", dir = back]; ipad -> client5555 [label = "subscribe", dir = back]; node [shape = plaintext]; message [label = "PUBLISH tweet.shop.kindle message", fillcolor = "#FADCAD"]; kindle [label = "tweet.shop.kindle", shape = ellipse, fillcolor = "#A8E270"]; pattern [label = "tweet.shop.*", shape = octagon]; message -> kindle [style = "bold, dashed", color = "#B22222"]; kindle -> pattern [style = "bold, dashed", color = "#B22222"]; node [shape = box]; kindle -> clientX [style = "bold, dashed", color = "#B22222", label = "message"]; kindle -> clientY [style = "bold, dashed", color = "#B22222", label = "message"]; pattern -> client123 [label = "message", style = "bold, dashed", color = "#B22222"]; pattern -> client256 [label = "message", style = "bold, dashed", color = "#B22222"]; // client color client123 [fillcolor = "#95BBE3"]; client256 [fillcolor = "#95BBE3"]; clientX [fillcolor = "#95BBE3"]; clientY [fillcolor = "#95BBE3"]; client3333 [fillcolor = "#95BBE3"]; client4444 [fillcolor = "#95BBE3"]; client5555 [fillcolor = "#95BBE3"];}](https://box.kancloud.cn/2015-09-13_55f4effd98208.svg)
另一方面,如果接收到信息的是頻道 `tweet.shop.ipad` ,那么 `client123` 和 `client256` 同樣會收到信息:
![digraph pattern_relation { rankdir = BT; node [style = filled]; edge [style = bold]; kindle [label = "tweet.shop.kindle", fillcolor = "#A8E270"]; ipad [label = "tweet.shop.ipad", fillcolor = "#A8E270"]; node [shape = octagon]; pattern [label = "tweet.shop.*"]; pattern -> kindle [label = "match"]; pattern -> ipad [style = "bold, dashed", color = "#B22222", dir = back]; node [shape = box]; client123 -> pattern [label = "message", dir = back, style= "bold, dashed", color = "#B22222"]; client256 -> pattern [label = "message", dir = back, style= "bold, dashed", color = "#B22222"]; clientX -> kindle [label = "subscribe"]; clientY -> kindle [label = "subscribe"]; client3333 -> ipad [label = "message", style = "bold, dashed", color = "#B22222", dir = back]; client4444 -> ipad [label = "message", style = "bold, dashed", color = "#B22222", dir = back]; client5555 -> ipad [label = "message", style = "bold, dashed", color = "#B22222", dir = back]; // new publish [label = "PUBLISH tweet.shop.ipad message", shape = plaintext, fillcolor = "#FADCAD"]; ipad -> publish [style = "bold, dashed", color = "#B22222", dir = back]; // client color client123 [fillcolor = "#95BBE3"]; client256 [fillcolor = "#95BBE3"]; clientX [fillcolor = "#95BBE3"]; clientY [fillcolor = "#95BBE3"]; client3333 [fillcolor = "#95BBE3"]; client4444 [fillcolor = "#95BBE3"]; client5555 [fillcolor = "#95BBE3"];}](https://box.kancloud.cn/2015-09-13_55f4effda0456.svg)
### 訂閱模式
`redisServer.pubsub_patterns` 屬性是一個鏈表,鏈表中保存著所有和模式相關的信息:
~~~
struct redisServer {
// ...
list *pubsub_patterns;
// ...
};
~~~
鏈表中的每個節點都包含一個 `redis.h/pubsubPattern` 結構:
~~~
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;
~~~
`client` 屬性保存著訂閱模式的客戶端,而 `pattern` 屬性則保存著被訂閱的模式。
每當調用 `PSUBSCRIBE` 命令訂閱一個模式時,程序就創建一個包含客戶端信息和被訂閱模式的 `pubsubPattern` 結構,并將該結構添加到 `redisServer.pubsub_patterns` 鏈表中。
作為例子,下圖展示了一個包含兩個模式的 `pubsub_patterns` 鏈表,其中 `client123` 和 `client256` 都正在訂閱 `tweet.shop.*` 模式:
![digraph publish_pattern { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; redisServer [label = "redisServer| ... |<pubsub_patterns> pubsub_patterns | ...", fillcolor = "#A8E270"]; pubsubPattern_1 [label = "pubsubPattern | client \n client123 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; pubsubPattern_2 [label = "pubsubPattern | client \n client256 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; redisServer:pubsub_patterns -> pubsubPattern_1; pubsubPattern_1 -> pubsubPattern_2;}](https://box.kancloud.cn/2015-09-13_55f4effda9d15.svg)
如果這時客戶端 `client10086` 執行 `PSUBSCRIBE broadcast.list.*` ,那么 `pubsub_patterns` 鏈表將被更新成這樣:
![digraph pubsub_pattern { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; redisServer [label = "redisServer| ... |<pubsub_patterns> pubsub_patterns | ...", fillcolor = "#A8E270"]; pubsubPattern_1 [label = "pubsubPattern | client \n client123 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; pubsubPattern_2 [label = "pubsubPattern | client \n client256 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; pubsubPattern_3 [label = "pubsubPattern | client \n client10086 | pattern \n broadcast.live.*", fillcolor = "#FFC1C1"]; redisServer:pubsub_patterns -> pubsubPattern_1; pubsubPattern_1 -> pubsubPattern_2; pubsubPattern_2 -> pubsubPattern_3;}](https://box.kancloud.cn/2015-09-13_55f4effdb1cd0.svg)
通過遍歷整個 `pubsub_patterns` 鏈表,程序可以檢查所有正在被訂閱的模式,以及訂閱這些模式的客戶端。
### 發送信息到模式
發送信息到模式的工作也是由 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令進行的,在前面講解頻道的時候,我們給出了這樣一段偽代碼,說它定義了 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令的行為:
~~~
def PUBLISH(channel, message):
# 遍歷所有訂閱頻道 channel 的客戶端
for client in server.pubsub_channels[channel]:
# 將信息發送給它們
send_message(client, message)
~~~
但是,這段偽代碼并沒有完整描述 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 命令的行為,因為 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 除了將 `message` 發送到所有訂閱 `channel` 的客戶端之外,它還會將 `channel` 和 `pubsub_patterns` 中的模式進行對比,如果 `channel` 和某個模式匹配的話,那么也將 `message` 發送到訂閱那個模式的客戶端。
完整描述 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html#publish "(in Redis 命令參考 v2.8)") 功能的偽代碼定于如下:
~~~
def PUBLISH(channel, message):
# 遍歷所有訂閱頻道 channel 的客戶端
for client in server.pubsub_channels[channel]:
# 將信息發送給它們
send_message(client, message)
# 取出所有模式,以及訂閱模式的客戶端
for pattern, client in server.pubsub_patterns:
# 如果 channel 和模式匹配
if match(channel, pattern):
# 那么也將信息發給訂閱這個模式的客戶端
send_message(client, message)
~~~
舉個例子,如果 Redis 服務器的 `pubsub_patterns` 狀態如下:
![digraph pubsub_pattern { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; redisServer [label = "redisServer| ... |<pubsub_patterns> pubsub_patterns | ...", fillcolor = "#A8E270"]; pubsubPattern_1 [label = "pubsubPattern | client \n client123 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; pubsubPattern_2 [label = "pubsubPattern | client \n client256 | pattern \n tweet.shop.*", fillcolor = "#95BBE3"]; pubsubPattern_3 [label = "pubsubPattern | client \n client10086 | pattern \n broadcast.live.*", fillcolor = "#FFC1C1"]; redisServer:pubsub_patterns -> pubsubPattern_1; pubsubPattern_1 -> pubsubPattern_2; pubsubPattern_2 -> pubsubPattern_3;}](https://box.kancloud.cn/2015-09-13_55f4effdbab5a.svg)
那么當某個客戶端發送信息 `"Amazon Kindle, $69."` 到 `tweet.shop.kindle` 頻道時,除了所有訂閱了 `tweet.shop.kindle` 頻道的客戶端會收到信息之外,客戶端 `client123` 和 `client256` 也同樣會收到信息,因為這兩個客戶端訂閱的 `tweet.shop.*` 模式和 `tweet.shop.kindle` 頻道匹配。
### 退訂模式
使用 [PUNSUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/punsubscribe.html#punsubscribe "(in Redis 命令參考 v2.8)") 命令可以退訂指定的模式,這個命令執行的是訂閱模式的反操作:程序會刪除 `redisServer.pubsub_patterns` 鏈表中,所有和被退訂模式相關聯的 `pubsubPattern` 結構,這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。
### 小結
- 訂閱信息由服務器進程維持的 `redisServer.pubsub_channels` 字典保存,字典的鍵為被訂閱的頻道,字典的值為訂閱頻道的所有客戶端。
- 當有新消息發送到頻道時,程序遍歷頻道(鍵)所對應的(值)所有客戶端,然后將消息發送到所有訂閱頻道的客戶端上。
- 訂閱模式的信息由服務器進程維持的 `redisServer.pubsub_patterns` 鏈表保存,鏈表的每個節點都保存著一個 `pubsubPattern` 結構,結構中保存著被訂閱的模式,以及訂閱該模式的客戶端。程序通過遍歷鏈表來查找某個頻道是否和某個模式匹配。
- 當有新消息發送到頻道時,除了訂閱頻道的客戶端會收到消息之外,所有訂閱了匹配頻道的模式的客戶端,也同樣會收到消息。
- 退訂頻道和退訂模式分別是訂閱頻道和訂閱模式的反操作。