Redis集群分區原理及客戶端源碼解析
前言
Redis作為公司主流的緩存伺服器,其主要原因是Redis官方3.0後推出集群版本,可以支持我們快速搭建高可用的集群環境。客戶端我們選用使用頻次最高且簡單成熟的Jedis,而在使用過程中我們也碰到了一些問題。
例如:
Jedis集群客戶端不支持讀寫分離,無法利用備庫資源,為什麼?
Jedis集群客戶端不支持pipeline批量命令,是否可以支持?
要回答這兩個問題需要對Redis及其客戶端Jedis有一定的了解,因此前面會先介紹Redis集群分區和客戶端源碼解析,最後一部分是我個人對上述問題的思考回答,整體分三個部分:
Redis集群分區原理。
JedisCluster客戶端源碼解析。
問題剖析和解決思路。
1. Redis集群分區原理
1.1 Redis集群分布概述
Redis集群沒有使用一致性hash,而是引入了哈希槽的概念。
一個Redis集群被劃分成16384個哈希槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽。
集群的每個節點負責一部分hash槽,舉個例子,比如當前集群有3個節點,那麼:
1.2 鍵(Key)空間分布演算法
在Redis集群中,我們擁有16384個Slot,這個數是固定的,我們存儲在Redis集群中的所有的鍵都會被映射到這些Slot中。
鍵(Key)到Slot的基本映射演算法如下:
HASH_SLOT = CRC16(key) mod 16384
經過簡單的計算就得到了當前key應該是存儲在哪個slot裡面。
鍵哈希標籤原理
鍵哈希標籤是一種可以讓用戶指定將一批鍵都能夠被存放在同一個槽中的實現方法,用戶唯一要做的就是按照既定規則生成key即可,規則如下:
基本來說,如果一個鍵包含一個 「{…}」 這樣的模式,只有 { 和 } 之間的字元串會被用來做哈希以獲取哈希槽。但是由於可能出現多個 { 或 },計算的演算法如下:
如果鍵包含一個 { 字元。
那麼在 { 的右邊就會有一個 }。
在 { 和 } 之間會有一個或多個字元,第一個 } 一定是出現在第一個 { 之後。
然後不是直接計算鍵的哈希,只有在第一個 { 和它右邊第一個 } 之間的內容會被用來計算哈希值。
例子:
比如這兩個鍵 .following 和 .followers 會被哈希到同一個哈希槽里,因為只有 user1000 這個子串會被用來計算哈希值。
鍵哈希標籤可以一定程度上利用MGET做批量操作,然而真實的應用場景中不可能把大量的key放到同一槽Slot中,容易造成key分布不均和單點問題,這也是某些場景下需要使用pipeline命令的原因。
1.3 Redis集群擴容過程
Redis集群分區很方便支持在線擴/縮容,以擴容為例,大致分3步:
創建節點
加入集群
槽位和數據(key)遷移
如何在集群數據遷移過程中,保證可用性 ?
MOVE重定向
很常見,不僅僅只發生在數據遷移過程中,例如我們要查詢集群中的某個key值,我們會將命令發送到某個節點,而如果這個key值對應的Slot不在這個節點,此時節點會返回MOVE,這時需要客戶端重新建立Slot與節點的映射關係,再次發送命令到正確的節點。
ASK重定向
如上圖三,Slot 12000從節點C到節點D的遷移過程中,首先Slot 12000依然屬於節點C,部分key(如key1)已經遷移到了節點D,這個時候就可能發生ASK重定向(客戶端從節點C查詢不到key1的數據,這時需要從節點D查詢一次數據)。ASK重定向的目的就是在於在查詢Slot的key已遷移的情況下通知客戶端再查詢一次IMPORTING節點D,與MOVE的根本區別就是Slot與節點的關係映射沒有改變,暫時不需要重新建立Slot與節點的映射關係。
Smart客戶端
因為集群節點不能代理(proxy)命令請求, 所以客戶端應該在節點返回 -MOVED 或者 -ASK 轉向(redirection)錯誤時, 自行將命令請求轉發至其他節點。
理論上來說, 客戶端是無須保存集群狀態信息的。不過, 如果客戶端可以將鍵和節點之間的映射信息保存起來, 可以有效地減少可能出現的轉向次數, 籍此提升命令執行的效率。
2. JedisCluster客戶端源碼解析
2.1 JedisCluster簡要類圖
2.2 JedisCluster初始化過程
JedisCluster集群初始化主要工作:保存節點和Slot的映射關係。
discoverClusterNodesAndSlots源碼解析:(加入了注釋便於理解)
2.3 JedisCluster方法調用
JedisCluster一個方法對應redis-cli的一條命令,非常便於理解和使用。
以get(key)為例的方法調用過程如下:
runWithRetries源碼解析:
分兩部分解析:
正常邏輯
獲取Connection
執行具體命令
異常處理
網路異常(重試)
重定向異常處理(MOVE、ASK)
3. 問題剖析和解決思路
3.1 Jedis客戶端不支持讀寫分離, 為什麼?
源碼分析
從JedisCluster初始化過程中已經看到,客戶端初始化時只保存了主(Master)的Slot與節點的對應關係。
JedisCluster方法調用過程只能獲取到Master節點的連接Jedis(Connection)。
為什麼JedisCluster不支持讀寫分離?
代碼複雜度,如要支持讀寫分離,還需要支持高可用的讀寫分離策略。
Redis集群數據同步為非強一致性和主從延遲都會導致數據不一致的情況發生,這個是本人認為JedisCluster不支持讀寫分離的主要原因。
如何支持讀寫分離思路:(前提是要在非強數據一致性場景使用)
參考discoverClusterNodesAndSlots源碼解析,JedisCluster初始化時,不光只建立主(Master)庫的Slot與節點的映射關係,還可以建立備庫的映射關係。
參考runWithRetries源碼解析,調用過程中獲取connection時,需要增加讀寫分離策略,getConnectionBySlot方法增加策略選擇主備節點。(註:備用的redis節點在正式發送命令前需要先發送Readonly命令)
3.2 JedisCluster不支持Pipeline批量命令,如何支持?源碼分析
JedisCluster源碼確實沒有提供pipeline方法。
而Jedis(connection)中是有pipeline方法的。
為什麼JedisCluster不支持Pipeline批量命令?
集群下keys分散到各個節點,pipeline批量命令可用性和性能沒法保證,不確定性太大,這是本人認為JedisCluster不去實現它的主要原因。
支持集群pipeline思路:(充分理解應用場景的情況下使用)
正如上面所說,不是因為pipeline實現很難所以源碼沒有提供,相反其實做集群的pipeline實現並不複雜,網上也能找到一些的實現方法。
pipeline方法的提供者是Jedis(Connection),因此只是找到Jedis,也就能進行pipeline方法調用。
接下來就是怎麼找到Jedis(connection)了,從源碼里看到只要找到Slot就可以找jedis(getConnectionBySlot),而Slot是可以通過Key計算出來的。
最後可以試著寫寫(多節點可以考慮用map reduce並發編程),偽代碼pipelineGet(List keys)邏輯如下:
獲取節點Jedis和keys的對應關係:Map> getConnectionsAndKeys(List keys)
並發(map)遍歷所有Jedis,通過jedis.pipeline()獲取values。
整合(reduce)所有keys-values返回結果。
最後
Redis集群的知識點還有很多很多,去中心化和分散式協議gossip,主從複製模型,失效檢測(容錯),選舉機制,數據最終一致性等都沒有在文中涉及,有興趣的可以私下交流溝通。
※健身私教了解一下
※19歲小伙喝百草枯,18天後不治身亡:珍愛生命,遠離毒藥
TAG:全球大搜羅 |