redis訂閱與發佈

訂閱與發佈

Redis 經過 PUBLISH 、 SUBSCRIBE 等命令實現了訂閱與發佈模式, 這個功能提供兩種信息機制, 分別是訂閱/發佈到頻道和訂閱/發佈到模式, 下文先討論訂閱/發佈到頻道的實現, 再討論訂閱/發佈到模式的實現。html

頻道的訂閱與信息發送

Redis 的 SUBSCRIBE 命令可讓客戶端訂閱任意數量的頻道, 每當有新信息發送到被訂閱的頻道時, 信息就會被髮送給全部訂閱指定頻道的客戶端。node

做爲例子, 下圖展現了頻道 channel1 , 以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關係:python

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"];
}

當有新消息經過 PUBLISH 命令發送給頻道 channel1 時, 這個消息就會被髮送給訂閱它的三個客戶端:redis

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"];
}

在後面的內容中, 咱們將探討 SUBSCRIBE 和 PUBLISH 命令的實現, 以及這套訂閱與發佈機制的運做原理。服務器

訂閱頻道

每一個 Redis 服務器進程都維持着一個表示服務器狀態的 redis.h/redisServer 結構, 結構的 pubsub_channels 屬性是一個字典, 這個字典就用於保存訂閱頻道的信息:app

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

其中,字典的鍵爲正在被訂閱的頻道, 而字典的值則是一個鏈表, 鏈表中保存了全部訂閱這個頻道的客戶端。url

好比說,在下圖展現的這個 pubsub_channels 示例中, client2 、 client5 和 client1 就訂閱了 channel1 , 而其餘頻道也分別被別的客戶端所訂閱:spa

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;
}

當客戶端調用 SUBSCRIBE 命令時, 程序就將客戶端和要訂閱的頻道在 pubsub_channels 字典中關聯起來。.net

舉個例子,若是客戶端 client10086 執行命令 SUBSCRIBE channel1 channel2 channel3 ,那麼前面展現的 pubsub_channels 將變成下面這個樣子:code

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;
}

SUBSCRIBE 命令的行爲能夠用僞代碼表示以下:

def SUBSCRIBE(client, channels): # 遍歷全部輸入頻道 for channel in channels: # 將客戶端添加到鏈表的末尾 redisServer.pubsub_channels[channel].append(client) 

經過 pubsub_channels 字典, 程序只要檢查某個頻道是否爲字典的鍵, 就能夠知道該頻道是否正在被客戶端訂閱; 只要取出某個鍵的值, 就能夠獲得全部訂閱該頻道的客戶端的信息。

發送信息到頻道

瞭解了 pubsub_channels 字典的結構以後, 解釋 PUBLISH 命令的實現就很是簡單了: 當調用 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;
}

PUBLISH 命令的實現能夠用如下僞代碼來描述:

def PUBLISH(channel, message): # 遍歷全部訂閱頻道 channel 的客戶端 for client in server.pubsub_channels[channel]: # 將信息發送給它們 send_message(client, message) 

退訂頻道

使用 UNSUBSCRIBE 命令能夠退訂指定的頻道, 這個命令執行的是訂閱的反操做: 它從 pubsub_channels 字典的給定頻道(鍵)中, 刪除關於當前客戶端的信息, 這樣被退訂頻道的信息就不會再發送給這個客戶端。

模式的訂閱與信息發送

當使用 PUBLISH 命令發送信息到某個頻道時, 不只全部訂閱該頻道的客戶端會收到信息, 若是有某個/某些模式和這個頻道匹配的話, 那麼全部訂閱這個/這些頻道的客戶端也一樣會收到信息。

下圖展現了一個帶有頻道和模式的例子, 其中 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"];
}

當有信息發送到 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"];


}

另外一方面, 若是接收到信息的是頻道 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"];



}

訂閱模式

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;
}

若是這時客戶端 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;
}

經過遍歷整個 pubsub_patterns 鏈表,程序能夠檢查全部正在被訂閱的模式,以及訂閱這些模式的客戶端。

發送信息到模式

發送信息到模式的工做也是由 PUBLISH 命令進行的, 在前面講解頻道的時候, 咱們給出了這樣一段僞代碼, 說它定義了 PUBLISH 命令的行爲:

def PUBLISH(channel, message): # 遍歷全部訂閱頻道 channel 的客戶端 for client in server.pubsub_channels[channel]: # 將信息發送給它們 send_message(client, message) 

可是,這段僞代碼並無完整描述 PUBLISH 命令的行爲, 由於 PUBLISH 除了將 message 發送到全部訂閱 channel 的客戶端以外, 它還會將 channel 和 pubsub_patterns 中的模式進行對比, 若是 channel 和某個模式匹配的話, 那麼也將 message 發送到訂閱那個模式的客戶端。

完整描述 PUBLISH 功能的僞代碼定於以下:

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;
}

那麼當某個客戶端發送信息 "Amazon Kindle, $69." 到 tweet.shop.kindle 頻道時, 除了全部訂閱了 tweet.shop.kindle 頻道的客戶端會收到信息以外, 客戶端 client123 和 client256 也一樣會收到信息, 由於這兩個客戶端訂閱的 tweet.shop.* 模式和 tweet.shop.kindle 頻道匹配。

退訂模式

使用 PUNSUBSCRIBE 命令能夠退訂指定的模式, 這個命令執行的是訂閱模式的反操做: 程序會刪除 redisServer.pubsub_patterns 鏈表中, 全部和被退訂模式相關聯的 pubsubPattern 結構, 這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。

小結

  • 訂閱信息由服務器進程維持的 redisServer.pubsub_channels 字典保存,字典的鍵爲被訂閱的頻道,字典的值爲訂閱頻道的全部客戶端。
  • 當有新消息發送到頻道時,程序遍歷頻道(鍵)所對應的(值)全部客戶端,而後將消息發送到全部訂閱頻道的客戶端上。
  • 訂閱模式的信息由服務器進程維持的 redisServer.pubsub_patterns 鏈表保存,鏈表的每一個節點都保存着一個 pubsubPattern 結構,結構中保存着被訂閱的模式,以及訂閱該模式的客戶端。程序經過遍歷鏈表來查找某個頻道是否和某個模式匹配。
  • 當有新消息發送到頻道時,除了訂閱頻道的客戶端會收到消息以外,全部訂閱了匹配頻道的模式的客戶端,也一樣會收到消息。
  • 退訂頻道和退訂模式分別是訂閱頻道和訂閱模式的反操做。
相關文章
相關標籤/搜索