當一個客戶端執行?SUBSCRIBE?命令, 訂閱某個或某些頻道的時候, 這個客戶端與被訂閱頻道之間就建立起了一種訂閱關系。
Redis 將所有頻道的訂閱關系都保存在服務器狀態的?`pubsub_channels`?字典里面, 這個字典的鍵是某個被訂閱的頻道, 而鍵的值則是一個鏈表, 鏈表里面記錄了所有訂閱這個頻道的客戶端:
~~~
struct redisServer {
// ...
// 保存所有頻道的訂閱關系
dict *pubsub_channels;
// ...
};
~~~
比如說, 圖 IMAGE_PUBSUB_CHANNELS 就展示了一個?`pubsub_channels`?字典示例, 這個字典記錄了以下信息:
* `client-1`?、?`client-2`?、?`client-3`?三個客戶端正在訂閱?`"news.it"`?頻道。
* 客戶端?`client-4`?正在訂閱?`"news.sport"`?頻道。
* `client-5`?和?`client-6`?兩個客戶端正在訂閱?`"news.business"`?頻道。

## 訂閱頻道
每當客戶端執行?SUBSCRIBE?命令, 訂閱某個或某些頻道的時候, 服務器都會將客戶端與被訂閱的頻道在?`pubsub_channels`?字典中進行關聯。
根據頻道是否已經有其他訂閱者, 關聯操作分為兩種情況執行:
* 如果頻道已經有其他訂閱者, 那么它在?`pubsub_channels`?字典中必然有相應的訂閱者鏈表, 程序唯一要做的就是將客戶端添加到訂閱者鏈表的末尾。
* 如果頻道還未有任何訂閱者, 那么它必然不存在于?`pubsub_channels`?字典, 程序首先要在?`pubsub_channels`?字典中為頻道創建一個鍵, 并將這個鍵的值設置為空鏈表, 然后再將客戶端添加到鏈表, 成為鏈表的第一個元素。
舉個例子, 假設服務器?`pubsub_channels`?字典的當前狀態如圖 IMAGE_PUBSUB_CHANNELS 所示, 那么當客戶端?`client-10086`?執行命令:
~~~
SUBSCRIBE "news.sport" "news.movie"
~~~
之后,?`pubsub_channels`?字典將更新至圖 IMAGE_AFTER_SUBSCRIBE 所示的狀態, 其中用虛線包圍的是新添加的節點:
* 更新后的?`pubsub_channels`?字典新增了?`"news.movie"`?鍵, 該鍵對應的鏈表值只包含一個?`client-10086`?節點, 表示目前只有?`client-10086`一個客戶端在訂閱?`"news.movie"`?頻道。
* 至于原本就已經有客戶端在訂閱的?`"news.sport"`?頻道,?`client-10086`?的節點放在了頻道對應鏈表的末尾, 排在?`client-4`?節點的后面。

SUBSCRIBE?命令的實現可以用以下偽代碼來描述:
~~~
def subscribe(*all_input_channels):
# 遍歷輸入的所有頻道
for channel in all_input_channels:
# 如果 channel 不存在于 pubsub_channels 字典(沒有任何訂閱者)
# 那么在字典中添加 channel 鍵,并設置它的值為空鏈表
if channel not in server.pubsub_channels:
server.pubsub_channels[channel] = []
# 將訂閱者添加到頻道所對應的鏈表的末尾
server.pubsub_channels[channel].append(client)
~~~
## 退訂頻道
UNSUBSCRIBE?命令的行為和?SUBSCRIBE?命令的行為正好相反 —— 當一個客戶端退訂某個或某些頻道的時候, 服務器將從?`pubsub_channels`?中解除客戶端與被退訂頻道之間的關聯:
* 程序會根據被退訂頻道的名字, 在?`pubsub_channels`?字典中找到頻道對應的訂閱者鏈表, 然后從訂閱者鏈表中刪除退訂客戶端的信息。
* 如果刪除退訂客戶端之后, 頻道的訂閱者鏈表變成了空鏈表, 那么說明這個頻道已經沒有任何訂閱者了, 程序將從?`pubsub_channels`?字典中刪除頻道對應的鍵。
舉個例子, 假設?`pubsub_channels`?的當前狀態如圖 IMAGE_BEFORE_UNSUBSCRIBE 所示, 那么當客戶端?`client-10086`?執行命令:
~~~
UNSUBSCRIBE "news.sport" "news.movie"
~~~
之后, 圖中用虛線包圍的兩個節點將被刪除, 如圖 IMAGE_AFTER_UNSUBSCRIBE 所示:
* 在?`pubsub_channels`?字典更新之后,?`client-10086`?的信息已經從?`"news.sport"`?頻道和?`"news.movie"`?頻道的訂閱者鏈表中被刪除了。
* 另外, 因為刪除?`client-10086`?之后, 頻道?`"news.movie"`?已經沒有任何訂閱者, 因此鍵?`"news.movie"`?也從字典中被刪除了。


UNSUBSCRIBE?命令的實現可以用以下偽代碼來描述:
~~~
def unsubscribe(*all_input_channels):
# 遍歷要退訂的所有頻道
for channel in all_input_channels:
# 在訂閱者鏈表中刪除退訂的客戶端
server.pubsub_channels[channel].remove(client)
# 如果頻道已經沒有任何訂閱者了(訂閱者鏈表為空)
# 那么將頻道從字典中刪除
if len(server.pubsub_channels[channel]) == 0:
server.pubsub_channels.remove(channel)
~~~
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤