Redis Cluster本身提供了自動将資料分散到Redis Cluster不同節點的能力,分區實作的關鍵點問題包括:如何将資料自動地打散到不同的節點,使得不同節點的存儲資料相對均勻;如何保證用戶端能夠通路到正确的節點和資料;如何保證重新分片的過程中不影響正常服務。這篇文章通過了解這些問題來認識Redis Cluster分區實作原理。
認識Redis Cluster
Redis Cluster是由多個同時服務于一個資料集合的Redis執行個體組成的整體,對于使用者來說,使用者隻關注這個資料集合,而整個資料集合的某個資料子集存儲在哪個節點對于使用者來說是透明的。Redis Cluster具有分布式系統的特點,也具有分布式系統如何實作高可用性與資料一緻性的難點,由多個Redis執行個體組成的Redis Cluster結構通常如下:

- 所有的節點互相連接配接;
- 叢集消息通信通過叢集總線通信,,叢集總線端口大小為用戶端服務端口+10000,這個10000是固定值;
- 節點與節點之間通過二進制協定進行通信;
- 用戶端和叢集節點之間通信和通常一樣,通過文本協定進行;
- 叢集節點不會代理查詢;
Redis Cluster分區實作原理
槽(slot)概念
Redis Cluster中有一個16384長度的槽的概念,他們的編号為0、1、2、3……16382、16383。這個槽是一個虛拟的槽,并不是真正存在的。正常工作的時候,Redis Cluster中的每個Master節點都會負責一部分的槽,當有某個key被映射到某個Master負責的槽,那麼這個Master負責為這個key提供服務,至于哪個Master節點負責哪個槽,這是可以由使用者指定的,也可以在初始化的時候自動生成(redis-trib.rb腳本)。這裡值得一提的是,在Redis Cluster中,隻有Master才擁有槽的所有權,如果是某個Master的slave,這個slave隻負責槽的使用,但是沒有所有權。Redis Cluster怎麼知道哪些槽是由哪些節點負責的呢?某個Master又怎麼知道某個槽自己是不是擁有呢?
位序列結構
Master節點維護着一個16384/8位元組的位序列,Master節點用bit來辨別對于某個槽自己是否擁有。比如對于編号為1的槽,Master隻要判斷序列的第二位(索引從0開始)是不是為1即可。
如上面的序列,表示目前Master擁有編号為1,134的槽。叢集同時還維護着槽到叢集節點的映射,是由長度為16384類型為節點的數組實作的,槽編号為數組的下标,數組内容為叢集節點,這樣就可以很快地通過槽編号找到負責這個槽的節點。位序列這個結構很精巧,即不浪費存儲空間,操作起來又很便捷。
鍵空間分布基本算法
這裡講的是Redis Cluster如何将鍵空間分布在不同的節點的,鍵空間意為Redis Cluster所擁有使用者所有資料集合的鍵的取值範圍,這個範圍叫做鍵空間。提到空間分布,必然會想到雜湊演算法,沒錯,通過雜湊演算法再加上取模運算可以将一個值固定地映射到某個區間,在這裡,這個區間叫做slots,區間由連續的slot組成。在Redis Cluster中,我們擁有16384個slot,這個數是固定的,我們存儲在Redis Cluster中的所有的鍵都會被映射到這些slot中,下面講講Redis Cluster是如何做映射的。
鍵到slot的基本映射算法如下:
HASH_SLOT = CRC16(key) mod 16384
用Redis中的代碼表示如下(這個代碼被稍微修改了一下,後面會還原):
crc16(key) & 0x3FFF
經過簡單的計算就得到了目前key應該是存儲在哪個slot裡面,值得注意的是,指定的key會被存儲在哪個slot,這個關系是鐵打不變的。如果我送出了一批指令,往Redis中存儲一批鍵,那麼這些鍵一般會被映射到不同的slot,而不同的slot又可能由Redis Cluster中不同的節點服務,這樣就和的預期有點不同,有沒有辦法将這批鍵映射到同一個slot呢?答案是可以。
鍵哈希标簽原理
鍵哈希标簽是一種可以讓使用者指定将一批鍵都能夠被存放在同一個槽中的實作方法,使用者唯一要做的就是按照既定規則生成key即可,這個規則是這樣的,如果我有對于同一個使用者有兩種不同含義的兩份資料,我隻要将他們的鍵設定為下面即可:
abc{userId}def和ghi{userId}jkl
redis在計算槽編号的時候隻會擷取{}之間的字元串進行槽編号計算,這樣由于上面兩個不同的鍵,{}裡面的字元串是相同的,是以他們可以被計算出相同的槽,相關代碼如下:
unsigned int keyHashSlot(char *key, int keylen) {
int s, e;
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
if (s == keylen) return crc16(key,keylen) & 0x3FFF;
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;
return crc16(key+s+1,e-s-1) & 0x3FFF;
}
用戶端是怎麼在Redis Cluster中找到正确的節點的呢?下面看看。
重定向用戶端
文章開始講到,Redis Cluster并不會代理查詢,那麼如果用戶端通路了一個key并不存在的節點,這個節點是怎麼處理的呢?比如我想擷取key為msg的值,msg計算出來的槽編号為254,目前節點正好不負責編号為254的槽,那麼就會傳回用戶端下面資訊:
GET msg
-MOVED 254 127.0.0.1:6381
表示用戶端想要的254槽由運作在IP為127.0.0.1,端口為6381的Master執行個體服務。如果根據key計算得出的槽恰好由目前節點負責,則當期節點會立即傳回結果。這裡明确一下,沒有代理的Redis Cluster可能會導緻用戶端兩次連接配接急群中的節點才能找到正确的服務,推薦用戶端緩存連接配接,這樣最壞的情況是兩次往返通信。
重新分片(Resharding)
重新分片意為槽到叢集節點的映射關系要改變,不變的是鍵到槽的映射關系,是以當重新分片的時候,如果槽中有鍵,那麼鍵也是要被移動到新的節點的。下面看看重新分片是怎麼做的,假如我們有一批槽需要從一個Master節點移動到另一個Master節點:
這裡簡化模型,假設這批待遷移的槽編号為1、2、3,并假設左邊的節點為MasterA節點,右邊的節點為MasterB節點。
槽遷移的過程
槽遷移的過程中有一個不穩定狀态,這個不穩定狀态會有一些規則,這些規則定義用戶端的行為,進而使得Redis Cluster不必當機的情況下可以執行槽的遷移。下面這張圖描述了我們遷移編号為1、2、3的槽的過程中,他們在MasterA節點和Master節點中的狀态。
MIGRATING狀态
本例中MIGRATING狀态是發生在MasterA節點中的一種槽的狀态,預備遷移槽的時候槽的狀态首先會變為MIGRATING狀态,這種狀态的槽會實際産生什麼影響呢?當用戶端請求的某個Key所屬的槽處于MIGRATING狀态的時候,影響有下面幾條:
- 如果Key存在則成功處理
- 如果Key不存在,則傳回用戶端ASK,僅當這次請求會轉向另一個節點,并不會重新整理用戶端中node的映射關系,也就是說下次該用戶端請求該Key的時候,還會選擇MasterA節點
- 如果Key包含多個指令,如果都存在則成功處理,如果都不存在,則傳回用戶端ASK,如果一部分存在,則傳回用戶端TRYAGAIN,通知用戶端稍後重試,這樣當所有的Key都遷移完畢的時候用戶端重試請求的時候回得到ASK,然後經過一次重定向就可以擷取這批鍵
IMPORTING狀态
本例中的IMPORTING狀态是發生在MasterB節點中的一種槽的狀态,預備将槽從MasterA節點遷移到MasterB節點的時候,槽的狀态會首先變為IMPORTING。IMPORTING狀态的槽對用戶端的行為有下面一些影響:
- 正常指令會被MOVED重定向,如果是ASKING指令則指令會被執行,進而Key沒有在老的節點已經被遷移到新的節點的情況可以被順利處理;
- 如果Key不存在則建立;
- 沒有ASKING的請求和正常請求一樣被MOVED,這保證用戶端node映射關系出錯的情況下不會發生寫錯;
鍵空間遷移
鍵空間遷移是指當滿足了槽遷移前提的情況下,我們就可以通過相關指令将槽1、2、3中的鍵空間從MasterA節點轉移到MasterB節點,這個過程真正實作資料轉移。相關指令:
MIGRATE
- DUMP
- RESTORE
- DEL
MIGRATE指令通過三步将資料轉移,示意圖如下:
經過上面三步可以将鍵空間資料遷移,然後再将處于MIGRATING和IMPORTING狀态的槽變為常态即可,完成整個重新分片的過程。然而MIGRATE并不是原子的,如果在MIGRATE出現錯誤的情況可能會導緻下面問題:
- 鍵空間在兩個節點都存在;
- 鍵空間隻存在第一個節點;
總結
本文介紹Redis Cluster分區實作原理主要關注三個問題,1)資料是如何被自動分散到不同的節點的;2)用戶端是如何能夠正确找到節點的;3)鍵空間遷移過程是怎麼樣的?其次是一些實作小細節,通過了解這些問題更好地了這些問題進而更好地認識Redis Cluster分區功能。
熬夜不易,點選請老王喝杯烈酒!!!!!!!