本文爲分佈式Redis深度歷險系列的第三篇,主要內容爲Redis的Clustor,也就是Redis集羣功能。html
更多文章見我的博客:github.com/farmerjohng…node
Redis集羣是Redis官方提供的分佈式方案,整個集羣經過將全部數據分紅16384個槽來進行數據共享。git
一個集羣由多個Redis節點組成,不一樣的節點經過CLUSTOR MEET
命令進行鏈接:github
CLUSTOR MEET <ip> <port>
redis
收到命令的節點會與命令中指定的目標節點進行握手,握手成功後目標節點會加入到集羣中,看個例子,圖片來自於Redis的設計與實現:算法
一個集羣的全部數據被分爲16384個槽,能夠經過CLUSTER ADDSLOTS
命令將槽指派給對應的節點。當全部的槽都有節點負責時,集羣處於上線狀態,不然處於下線狀態不對外提供服務。數組
clusterNode的位數組slots表明一個節點負責的槽信息。緩存
struct clusterNode {
unsigned char slots[16384/8]; /* slots handled by this node */
int numslots; /* Number of slots handled by this node */
...
}
複製代碼
看個例子,下圖中一、三、五、八、九、10位的值爲1,表明該節點負責槽一、三、五、八、九、10。bash
每一個Redis Server上都有一個ClusterState的對象,表明了該Server所在集羣的信息,其中字段slots記錄了集羣中全部節點負責的槽信息。服務器
typedef struct clusterState {
// 負責處理各個槽的節點
// 例如 slots[i] = clusterNode_A 表示槽 i 由節點 A 處理
// slots[i] = null 表明該槽目前沒有節點負責
clusterNode *slots[REDIS_CLUSTER_SLOTS];
}
複製代碼
能夠經過redis-trib工具對槽從新分配,重分配的實現步驟以下:
CLUSTER GETKEYSINSLOT <slot> <count>
從源節點獲取最多count個槽slot的keyMIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
命令,將被選中的鍵原子的從源節點遷移至目標節點。在槽重分配的過程當中,槽中的一部分數據保存着源節點,另外一部分保存在目標節點。這時若是要客戶端向源節點發送一個命令,且相關數據在一個正在遷移槽中,源節點處理步驟如圖:
當客戶端收到一個ASK錯誤的時候,會根據返回的信息向目標節點從新發起一次請求。
ASK和MOVED的區別主要是ASK是一次性的,MOVED是永久性的,有點像Http協議中的301和302。
咱們來看clustor下一次命令的請求過程,假設執行命令 get testKey
clustor client在運行前須要配置若干個server節點的ip和port。咱們稱這些節點爲種子節點。
clustor的客戶端在執行命令時,會先經過計算獲得key的槽信息,計算規則爲:getCRC16(key) & (16384 - 1)
,獲得槽信息後,會從一個緩存map中得到槽對應的redis server信息,若是能獲取到,則調到第4步
向種子節點發送slots
命令以得到整個集羣的槽分佈信息,而後跳轉到第2步重試命令
向負責該槽的server發起調用 server處理如圖:
客戶端若是收到MOVED錯誤,則根據對應的地址跳轉到第4步從新請求,
客戶段若是收到ASK錯誤,則根據對應的地址跳轉到第4步從新請求,並在請求前帶上ASKING標識。
以上步驟大體就是redis clustor下一次命令請求的過程,但忽略了一個細節,若是要查找的數據鎖所在的槽正在重分配怎麼辦?
集羣中每一個Redis節點都會按期的向集羣中的其餘節點發送PING消息,若是目標節點沒有在有效時間內回覆PONG消息,則會被標記爲疑似下線。同時將該信息發送給其餘節點。當一個集羣中有半數負責處理槽的主節點都將某個節點A標記爲疑似下線後,那麼A會被標記爲已下線,將A標記爲已下線的節點會將該信息發送給其餘節點。
好比說有A,B,C,D,E 5個主節點。E有F、G兩個從節點。 當E節點發生異常後,其餘節點發送給A的PING消息將不能獲得正常回復。當過了最大超時時間後,假設A,B先將E標記爲疑似下線;以後C也會將E標記爲疑似下線,這時C發現集羣中由3個節點(A、B、C)都將E標記爲疑似下線,超過集羣複製槽的主節點個數的一半(>2.5)則會將E標記爲已下線,並向集羣廣播E下線的消息。
當F、G(E的從節點)收到E被標記已下線的消息後,會根據Raft算法選舉出一個新的主節點,新的主節點會將E複製的全部槽指派給本身,而後向集羣廣播消息,通知其餘節點新的主節點信息。
選舉新的主節點算法與選舉Sentinel頭節點的過程很像:
集羣的配置紀元是一個自增計數器,它的初始值爲0.
當集羣裏的某個節點開始一次故障轉移操做時,集羣配置紀元的值會被增一。
對於每一個配置紀元,集羣裏每一個負責處理槽的主節點都有一次投票的機會,而第一個向主節點要求投票的從節點將得到主節點的投票。
檔從節點發現本身正在複製的主節點進入已下線狀態時,從節點會想集羣廣播一條CLUSTER_TYPE_FAILOVER_AUTH_REQUEST消息,要求全部接收到這條消息、而且具備投票權的主節點向這個從節點投票。
若是一個主節點具備投票權(它正在負責處理槽),而且這個主節點還沒有投票給其餘從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支持從節點成爲新的主節點。
每一個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,並根據本身收到了多少條這種消息來同濟本身得到了多少主節點的支持。
若是集羣裏有N個具備投票權的主節點,那麼當一個從節點收集到大於等於N/2+1張支持票時,這個從節點就會當選爲新的主節點。
由於在每個配置紀元裏面,每一個具備投票權的主節點只能投一次票,因此若是有N個主節點進行投票,那麼具備大於等於N/2+1張支持票的從節點只會有一個,這確保了新的主節點只會有一個。
若是在一個配置紀元裏面沒有從節點能收集到足夠多的支持票,那麼集羣進入一個新的配置紀元,並再次進行選舉,知道選出新的主節點爲止。
最後,聊聊redis集羣的其餘兩種實現方案。
客戶端作路由,採用一致性hash算法,將key映射到對應的redis節點上。 其優勢是實現簡單,沒有引用其餘中間件。 缺點也很明顯:是一種靜態分片方案,擴容性差。
Jedis中的ShardedJedis是該方案的實現。
該方案在client與redis之間引入一個代理層。client的全部操做都發送給代理層,由代理層實現路由轉發給不一樣的redis服務器。
其優勢是: 路由規則可自定義,擴容方便。 缺點是: 代理層有單點問題,多一層轉發的網絡開銷
其開源實現有twitter的twemproxy 和豌豆莢的codis
分佈式redis深度歷險系列到此爲止了,以後一個系列會詳細講講單機Redis的實現,包括Redis的底層數據結構、對內存佔用的優化、基於事件的處理機制、持久化的實現等等偏底層的內容,敬請期待~