一個 Redis 集群通常由多個節點(node)組成, 在剛開始的時候, 每個節點都是相互獨立的, 它們都處于一個只包含自己的集群當中, 要組建一個真正可工作的集群, 我們必須將各個獨立的節點連接起來, 構成一個包含多個節點的集群。
連接各個節點的工作可以使用?CLUSTER MEET?命令來完成, 該命令的格式如下:
~~~
CLUSTER MEET <ip> <port>
~~~
向一個節點?`node`?發送?CLUSTER MEET?命令, 可以讓?`node`?節點與?`ip`?和?`port`?所指定的節點進行握手(handshake), 當握手成功時,?`node`節點就會將?`ip`?和?`port`?所指定的節點添加到?`node`?節點當前所在的集群中。
舉個例子, 假設現在有三個獨立的節點?`127.0.0.1:7000`?、?`127.0.0.1:7001`?、?`127.0.0.1:7002`?(下文省略 IP 地址,直接使用端口號來區分各個節點), 我們首先使用客戶端連上節點 7000 , 通過發送?CLUSTER NODE?命令可以看到, 集群目前只包含 7000 自己一個節點:
~~~
$ redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER NODES
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
~~~
通過向節點 7000 發送以下命令, 我們可以將節點 7001 添加到節點 7000 所在的集群里面:
~~~
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
~~~
繼續向節點 7000 發送以下命令, 我們可以將節點 7002 也添加到節點 7000 和節點 7001 所在的集群里面:
~~~
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
~~~
現在, 這個集群里面包含了 7000 、 7001 和 7002 三個節點, 圖 IMAGE_CONNECT_NODES_1 至 IMAGE_CONNECT_NODES_5 展示了這三個節點進行握手的整個過程。





本節接下來的內容將介紹啟動節點的方法, 和集群有關的數據結構, 以及?CLUSTER MEET?命令的實現原理。
## 啟動節點
一個節點就是一個運行在集群模式下的 Redis 服務器, Redis 服務器在啟動時會根據?`cluster-enabled`?配置選項的是否為?`yes`?來決定是否開啟服務器的集群模式, 如圖 IMAGE_NODE_OR_SERVER 所示。

節點(運行在集群模式下的 Redis 服務器)會繼續使用所有在單機模式中使用的服務器組件, 比如說:
* 節點會繼續使用文件事件處理器來處理命令請求和返回命令回復。
* 節點會繼續使用時間事件處理器來執行?`serverCron`?函數, 而?`serverCron`?函數又會調用集群模式特有的?`clusterCron`?函數:?`clusterCron`函數負責執行在集群模式下需要執行的常規操作, 比如向集群中的其他節點發送 Gossip 消息, 檢查節點是否斷線; 又或者檢查是否需要對下線節點進行自動故障轉移, 等等。
* 節點會繼續使用數據庫來保存鍵值對數據,鍵值對依然會是各種不同類型的對象。
* 節點會繼續使用 RDB 持久化模塊和 AOF 持久化模塊來執行持久化工作。
* 節點會繼續使用發布與訂閱模塊來執行?PUBLISH?、?SUBSCRIBE?等命令。
* 節點會繼續使用復制模塊來進行節點的復制工作。
* 節點會繼續使用 Lua 腳本環境來執行客戶端輸入的 Lua 腳本。
諸如此類。
除此之外, 節點會繼續使用?`redisServer`?結構來保存服務器的狀態, 使用?`redisClient`?結構來保存客戶端的狀態, 至于那些只有在集群模式下才會用到的數據, 節點將它們保存到了?`cluster.h/clusterNode`?結構,?`cluster.h/clusterLink`?結構, 以及?`cluster.h/clusterState`?結構里面, 接下來的一節將對這三種數據結構進行介紹。
## 集群數據結構
`clusterNode`?結構保存了一個節點的當前狀態, 比如節點的創建時間, 節點的名字, 節點當前的配置紀元, 節點的 IP 和地址, 等等。
每個節點都會使用一個?`clusterNode`?結構來記錄自己的狀態, 并為集群中的所有其他節點(包括主節點和從節點)都創建一個相應的`clusterNode`?結構, 以此來記錄其他節點的狀態:
~~~
struct clusterNode {
// 創建節點的時間
mstime_t ctime;
// 節點的名字,由 40 個十六進制字符組成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
// 節點標識
// 使用各種不同的標識值記錄節點的角色(比如主節點或者從節點),
// 以及節點目前所處的狀態(比如在線或者下線)。
int flags;
// 節點當前的配置紀元,用于實現故障轉移
uint64_t configEpoch;
// 節點的 IP 地址
char ip[REDIS_IP_STR_LEN];
// 節點的端口號
int port;
// 保存連接節點所需的有關信息
clusterLink *link;
// ...
};
~~~
`clusterNode`?結構的?`link`?屬性是一個?`clusterLink`?結構, 該結構保存了連接節點所需的有關信息, 比如套接字描述符, 輸入緩沖區和輸出緩沖區:
~~~
typedef struct clusterLink {
// 連接的創建時間
mstime_t ctime;
// TCP 套接字描述符
int fd;
// 輸出緩沖區,保存著等待發送給其他節點的消息(message)。
sds sndbuf;
// 輸入緩沖區,保存著從其他節點接收到的消息。
sds rcvbuf;
// 與這個連接相關聯的節點,如果沒有的話就為 NULL
struct clusterNode *node;
} clusterLink;
~~~
`redisClient`?結構和?`clusterLink`?結構的相同和不同之處
`redisClient`?結構和?`clusterLink`?結構都有自己的套接字描述符和輸入、輸出緩沖區, 這兩個結構的區別在于,?`redisClient`?結構中的套接字和緩沖區是用于連接客戶端的, 而?`clusterLink`?結構中的套接字和緩沖區則是用于連接節點的。
最后, 每個節點都保存著一個?`clusterState`?結構, 這個結構記錄了在當前節點的視角下, 集群目前所處的狀態 —— 比如集群是在線還是下線, 集群包含多少個節點, 集群當前的配置紀元, 諸如此類:
~~~
typedef struct clusterState {
// 指向當前節點的指針
clusterNode *myself;
// 集群當前的配置紀元,用于實現故障轉移
uint64_t currentEpoch;
// 集群當前的狀態:是在線還是下線
int state;
// 集群中至少處理著一個槽的節點的數量
int size;
// 集群節點名單(包括 myself 節點)
// 字典的鍵為節點的名字,字典的值為節點對應的 clusterNode 結構
dict *nodes;
// ...
} clusterState;
~~~
以前面介紹的 7000 、 7001 、 7002 三個節點為例, 圖 IMAGE_CLUSTER_STATE_OF_7000 展示了節點 7000 創建的?`clusterState`?結構, 這個結構從節點 7000 的角度記錄了集群、以及集群包含的三個節點的當前狀態 (為了空間考慮,圖中省略了?`clusterNode`?結構的一部分屬性):
* 結構的?`currentEpoch`?屬性的值為?`0`?, 表示集群當前的配置紀元為?`0`?。
* 結構的?`size`?屬性的值為?`0`?, 表示集群目前沒有任何節點在處理槽: 因此結構的?`state`?屬性的值為?`REDIS_CLUSTER_FAIL`?—— 這表示集群目前處于下線狀態。
* 結構的?`nodes`?字典記錄了集群目前包含的三個節點, 這三個節點分別由三個?`clusterNode`?結構表示: 其中?`myself`?指針指向代表節點 7000 的?`clusterNode`?結構, 而字典中的另外兩個指針則分別指向代表節點 7001 和代表節點 7002 的?`clusterNode`?結構, 這兩個節點是節點 7000 已知的在集群中的其他節點。
* 三個節點的?`clusterNode`?結構的?`flags`?屬性都是?`REDIS_NODE_MASTER`?,說明三個節點都是主節點。
節點 7001 和節點 7002 也會創建類似的?`clusterState`?結構:
* 不過在節點 7001 創建的?`clusterState`?結構中,?`myself`?指針將指向代表節點 7001 的?`clusterNode`?結構, 而節點 7000 和節點 7002 則是集群中的其他節點。
* 而在節點 7002 創建的?`clusterState`?結構中,?`myself`?指針將指向代表節點 7002 的?`clusterNode`?結構, 而節點 7000 和節點 7001 則是集群中的其他節點。

## CLUSTER MEET 命令的實現
通過向節點 A 發送?CLUSTER MEET?命令, 客戶端可以讓接收命令的節點 A 將另一個節點 B 添加到節點 A 當前所在的集群里面:
~~~
CLUSTER MEET <ip> <port>
~~~
收到命令的節點 A 將與節點 B 進行握手(handshake), 以此來確認彼此的存在, 并為將來的進一步通信打好基礎:
1. 節點 A 會為節點 B 創建一個?`clusterNode`?結構, 并將該結構添加到自己的?`clusterState.nodes`?字典里面。
2. 之后, 節點 A 將根據?CLUSTER MEET?命令給定的 IP 地址和端口號, 向節點 B 發送一條?`MEET`?消息(message)。
3. 如果一切順利, 節點 B 將接收到節點 A 發送的?`MEET`?消息, 節點 B 會為節點 A 創建一個?`clusterNode`?結構, 并將該結構添加到自己的?`clusterState.nodes`?字典里面。
4. 之后, 節點 B 將向節點 A 返回一條?`PONG`?消息。
5. 如果一切順利, 節點 A 將接收到節點 B 返回的?`PONG`?消息, 通過這條?`PONG`?消息節點 A 可以知道節點 B 已經成功地接收到了自己發送的?`MEET`?消息。
6. 之后, 節點 A 將向節點 B 返回一條?`PING`?消息。
7. 如果一切順利, 節點 B 將接收到節點 A 返回的?`PING`?消息, 通過這條?`PING`?消息節點 B 可以知道節點 A 已經成功地接收到了自己返回的?`PONG`?消息, 握手完成。
圖 IMAGE_HANDSHAKE 展示了以上步驟描述的握手過程。

之后, 節點 A 會將節點 B 的信息通過 Gossip 協議傳播給集群中的其他節點, 讓其他節點也與節點 B 進行握手, 最終, 經過一段時間之后, 節點 B 會被集群中的所有節點認識。
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤