1. 程式人生 > >redis知識盤點【伍】_一致性雜湊和cluster叢集

redis知識盤點【伍】_一致性雜湊和cluster叢集

正如上文所講,sentinel解決的是redis叢集高可用問題,那麼當我們系統快取的資料量非常大,不再適合全量放在一個redis例項中的時候,redis 3.0版本推出的cluster功能就可以大展拳腳了。cluster雖然名字翻譯過來是叢集的意思,實際上它解決的是資料sharding問題,可以根據一定規則,將不同key的資料路由到不同的redis例項中儲存。而在redis cluster功能開放之前,我們基本都是自己實現sharding功能的,這裡就不得不提一致性雜湊(Consistent Hashing)演算法。

一致性雜湊演算法

首先說下什麼是雜湊演算法。雜湊演算法就是將任意長度的二進位制值對映為較短的固定長度的唯一的二進位制值(即雜湊值)。敲黑板,雜湊演算法的入參可以是任意長度,而出參是固定長度而且唯一。

而一致性雜湊演算法就是,先構造一個0到2^32的整數環(hash環,java中可用SortedMap實現),根據快取伺服器名稱(也可以是ip:port)計算出hash值,根據其hash值將快取伺服器放置在hash環上。每次根據要快取的key計算得到hash值,在hash環上順時針查詢距離最近的快取伺服器節點(SortedMap.tailMap(key)實現),進行set/set操作。原理如下圖所示:


但是一致性雜湊演算法有如下問題:

1.加減快取伺服器節點會造成hash環部分資料無法命中;

2.少量快取伺服器節點時,資料分佈不均勻,同時快取伺服器節點變化將影響大範圍資料;

3.普通的一致性雜湊分割槽需要增加一倍或減掉一半快取伺服器

節點才能保持資料負載均衡;

當只有少量快取伺服器又想盡量保證負載均衡時,我們一般採用下面的辦法:將一個快取伺服器節點虛擬成一組,比如某臺快取伺服器為192.168.2.1:6379,我們就可以將其虛擬成一個虛擬節點陣列,為192.168.2.1:6379-1,192.168.2.1:6379-2,192.168.2.1:6379-3……然後分別計算數組裡的元素的hash值並對映到hash環上,每臺快取伺服器都如此處理。當不同key的快取路由到虛擬節點時,最終都是指向真實的快取伺服器節點。通過這樣增加節點的方式,可以一定概率上使資料路由均衡。當然這也不是最好的解決方案,於是redis cluster橫空出世了。

redis cluster叢集

redis cluster使用資料分片(sharding)而非一致性雜湊(consistency hashing)來實現: 一個 Redis 叢集包含16384個雜湊槽(slot), 每個 key 都屬於這16384個雜湊槽的其中一個, 叢集使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽, 其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和 。槽(slot)是redis cluster中資料管理和遷移的基本單位,每個節點負責一定數量的槽。

cluster nodes配置很簡單,首先將.conf配置檔案中的cluster-enabled修改為yes,然後登陸某臺redis伺服器,執行cluster meet {ip} {port}去和叢集中其他redis伺服器握手,cluster nodes和cluster info命令可分別查詢叢集的節點和狀態。

我們還可以為登陸的redis伺服器設定一個從節點:cluster replicate {nodeid},其中nodeid為節點id,可通過cluster nodes命令查詢。一個主節點可以社多多個從節點,當主節點掛掉時,redis cluster會選取一個從節點升級為主節點繼續提供服務,只有當該節點的所有主從伺服器全部掛掉,redis cluster才停止提供服務。

當我們配置好redis叢集中的各個主從節點後沒救可以進行槽(slot)的分配了,命令為cluster addslots {slot_index1} {slot_index 2}  {slot_index 3} ...(此處槽id只支援一個一個指派,不過可以用linux迴圈命令執行)。注意,主要當16384個槽全部指派完畢後,redis cluster才會進入上線狀態。

當進行redis叢集擴容的時候,操作主要分三步,一是準備新節點;二是加入叢集;三是進行槽和資料遷移。這裡說說第三步的執行命令:

1.對目標節點發送

cluster setslot {slot_index} importing {source_node_id}

2.對源節點發送

cluster setslot {slot_index} migrating {target_node_id}

3.源節點迴圈執行

cluster getkeysinslot {slot_index} {count(key個數)}

4.源節點執行,把key通過流水線(pipeline)遷移到目標節點

migrate {target_ip} {target_port}  "" 0 {timeout} keys {key1} {key2} {key3}

5.重複3、4步驟

6.向叢集中所有主節點發送通知

cluster setslot {slot_index} node {target_nodeid}

【注】新版本提供了REDIS-MIGRATE-TOOL工具可以使用。

大概說一下redis cluster的內部實現,redis cluster中每個節點都維護了clusterNode和clusterState兩種結構體(當然還有其他元素,這裡只介紹這兩個)。其中,每個節點不止維護了自身的clusterNode,還儲存了叢集中其他節點(無論主從)的clusterNode,會通過叢集總的廣播進行更新。clusterNode中有一個slots屬性,是個二進位制陣列,長度為16384/8=2048,用每個二進位制位上的0還是1來表示該節點是否負責slot[i],如下圖所示:


而clusterState結構體中維護了一個*slot[16384]的指標陣列,記錄著叢集中節點和槽的對應關係。每個槽對應的slot指標資料下標,直接指向對應節點的clusterNode結構體。這裡將槽的指派資訊又記錄一遍的設計,是為了方便節點查詢直接返回,而不是遍歷一遍自己儲存的叢集總所有節點的clusterNode。

節點在接到命令請求時,會根據key所在的槽id去查clusterState.slot[槽id]是否指向自己的clusterNode,如果是則進行處理;如果不是則返回moved錯誤moved錯誤攜帶正確的節點ip和埠號返回給客戶端指引其轉向執行,而且客戶端以後的每一次關於該key都會去moved錯誤提供的節點去執行。

當節點的key正在遷移的時候,收到關於該key的請求,那麼節點會返回ask錯誤,並但會正確的節點ip和埠號給客戶端去執行。但是這個轉向只對本次請求有效,後面關於該key的請求還是會發送到目前正在處理key遷移的節點,直到key遷移完畢併發送廣播通知。

當然redis cluster也不是完美的,它有一些功能上的限制:
1.目前只支援同一個槽上的key的批量操作;

2.目前只支援同一個槽上的key事務;

3.只能使用資料庫0(每個redis例項有16個數據庫,可通過select {index}命令來切換);

4.不能將一個大的key(如hash、list)對映到不同的節點上;

5.目前叢集主從複製只支援一層,不支援巢狀樹狀架構;

到這裡感覺redis比較核心的東西都寫完了,下一篇寫點什麼我再想想。