Redis叢集是Redis提供的分散式資料庫方案,叢集通過分片來進行資料共享,並提供複製和故障轉移操作。
一個Redis叢集通常由多個節點組成,在剛開始的時候每個節點都是相互獨立的,他們處於一個只包含自己的叢集當中,我們通過使用CLUSTER MEET命令將節點連線到一起,構成一個包含多節點的叢集。
叢集的資料結構:
clusterNode結構儲存了一個節點的當前狀態,比如節點建立時間、節點名稱、節點當前的配置紀元、節點的ip埠。每個節點都會使用一個clusterNode結構記錄自己的狀態,併為叢集中的所有其他節點都建立一個相應的clusterNode結構。
struct clusterNode{ //建立節點的時間 mstime_t ctime; //節點的名稱,由40個十六進位制字元組成 char name[REDIS_C:USTER_NAMELEN] //節點標識(標識節點的角色以及節點目前狀態) inf flags; //節點當前的配置紀元 uint64_t configEpochl; //節點的ip地址 char ip; //節點的埠號 int port; //儲存連線節點所需要的有關資訊 clusterLink *link; }
typedef struct clusterLink{ //連線的建立時間 mstime_t ctime; //TCP 套接字描述 int fd; //輸出緩衝區,儲存著等待傳送給其他節點的訊息 sds sndbuf; //輸入緩衝區,儲存著從其他節點接收到的訊息 sds rcvbuf; //與這個連線相關聯的節點,如果沒有的話為NULL struct clusterNode *node; } clusterLink;
每個節點都儲存著一個clusterState結構,這個結構記錄了在當前節點的視角下,叢集目前所處的狀態,
typedef struct clusterState{ //指向當前節點的指標 clusterNode *myself; //叢集當前的配置紀元,用於實現故障轉移。 uint64_t currentEpoch; //叢集當前的狀態:線上或下線 int state; //叢集中至少處理著一個槽的節點數量 int size; //叢集節點名單(包括myself節點) //字典的鍵為節點的名稱,字典值為節點對應的clusterNode結構 dict *node; } clusterState;
槽指派:
Redis叢集通過分片的方式來儲存資料庫中的鍵值對:叢集的整個資料庫被分成16348個槽,資料庫中的每個鍵都屬於16384個槽的其中一個,叢集中的每個節點可以處理0個最多16384個槽。
使用cluster meet 命令將節點連線到叢集裡面後,這時叢集仍處於下線狀態,因為叢集中的節點沒有處理任何槽
通過使用cluster addslots < slot > 命令,可以為節點分配槽
記錄節點的槽指派資訊:
clusterNode 結構的slots屬性和numslot屬性記錄了節點負責處理那些槽:
struct clusterNode{ unsigned char slots[16348/8]; int numslots; };
同時,節點會將自己的slots陣列通過訊息傳送給叢集中的其他節點,以此來告知其他節點自己目前負責處理那些槽。
clusterState結構中的slots陣列記錄了叢集中所有16384個槽的指派資訊。
typedef struct clusterState{ clusterNode *slots[16384]; }clusterState;
clusterState.slots是為了更快的定位槽所在的節點O(i)。
clusterNode.slots 當程式需要將某個節點的槽指派資訊通過訊息傳送給其他節點時,程式只需要將相應節點的clusterNode.slots陣列整個傳送過去就可以,clusterState.slots記錄了叢集中所有的槽指派訊息,而clusterNode.slots只記錄了當前節點的槽指派資訊。
當客戶端向節點傳送與資料庫鍵有關的命令時,接收命令的節點會計算出命令要處理的資料庫鍵屬於哪個槽,並檢查這個槽是否指派給了自己:
如果鍵所在的槽正好是指派給了當前節點,那麼節點直接執行這個命令;如果鍵所在的槽並沒有指派給當前節點,那麼節點會向客戶端返回一個MOVED錯誤,指引客戶端轉向到正確節點,並再次傳送之前想要執行的命令。
節點使用以下演算法來計算給定鍵key屬於哪個槽:
def slot_number(key): return CRC16(key) & 16383
當節點計算出鍵所屬的槽i之後,節點就會檢查自己在clusterState.slots陣列中的項i,判斷所在的槽是否由自己負責:如果clusterState.slots[i]等於clusterState.myself,那麼說明槽i由當前節點負責,節點可以執行客戶端傳送的命令;反之節點會根據slusterState.slots[i]指向的clusterNode結構所記錄的節點IP和埠號,向客戶端返回MOVED錯誤指引客戶端轉向至再處理槽i的節點。
MOVED錯誤的格式為:MOVED < slot > <ip>:<port>
當客戶端接收到節點返回的MOVED錯誤時,客戶端根據MOVED錯誤提供的IP地址和埠號,轉向至負責處理槽slot的節點,並向該節點重新傳送之前想要執行的命令。一個叢集客戶端通常會與叢集中的多個節點建立套接字連線,而所謂的節點轉向實際上就是換一個套接字來傳送命令。
叢集模式的redis-cli 客戶端在接收到MOVED錯誤時,並不會列印出MOVED錯誤,而是根據MOVED錯誤自動進行節點轉向,並列印出轉向資訊,所以我們時看不見節點返回的MOVED錯誤。
節點和單機伺服器在資料庫方面的一個區別時,節點只能使用0號資料庫,而單機Redis伺服器則沒有這一限制。除了將鍵值對儲存在資料庫裡面之外,節點還會用clusterState結構中slots_to_keys跳躍表來儲存槽和鍵之間的關係:
typedef struct clusterState{ zskiplist *slots_to_keys; } clusterState;
slots_to_keys跳躍表每個節點的分值score都是一個槽號,而每個節點的成員(member)都是一個資料庫鍵:每當節點往資料庫中新增一個新的鍵值對時,節點就會將這個鍵以及鍵的槽號關聯到slots_to_keys跳躍表;當節點刪除資料庫中的每個鍵值對時,節點就會在slots_to_keys跳躍表解除被刪除鍵與槽號的關聯。
通過在slots_to_keys跳躍表中記錄各個資料庫鍵所屬的槽,節點可以很方便地對屬於某個或某些槽的所有資料庫鍵進行批量操作。
Redis叢集的重新分片操作可以將任意數量已經指派給某個節點(源節點)的槽改為指派給另一個節點,並且相關槽所屬的鍵值對也會從源節點被移動到目標節點。重新分派操作可以線上進行,在重新分片的過程中,叢集不需要下線,並且源節點和目標節點都可以繼續處理命令請求。
Redis叢集的重新分片操作是由Redis的叢集管理軟體redis-trib負責執行的,Redis提供了進行重新分片所需要的所有命令,而redis-trib則通過源節點和目標節點傳送命令來進行重新分片操作。
1)redis-trib對目標節點傳送CLUSTER SETSLOT < slot > IMPORTING <source_id >命令,讓目標節點準備好從源節點匯入屬於槽slot的鍵值對。
2)redis-trib對CLUSTER SETSLOT< slot > MIGATING < target_id > 命令,讓源節點準備好將屬於槽slot的鍵值對遷移至目標節點。
3)redis-trib向源節點傳送CLUSTER GETKEYSINGSLOT < slot > < count > 獲得最多count 個屬於槽slot的鍵值對的鍵名。
4)對於步驟3獲得的鍵名,redis-trib都向源節點傳送一個MIGRATE < target_ip > < target_port > < key_name > 0 <timeout> 命令,將被選中的鍵原子地從源節點遷移至目標節點。
5)重複 3,4步驟,直到所有鍵值對都被遷移至目標節點。
6)redis-trib向叢集中的任意一個節點傳送CLUSTER SETSLOT < slot > NODE < target_id > 命令,將槽slot指派給目標節點,這一指派資訊通過訊息傳送至整個叢集,最終叢集中的所有節點都會直到槽slot已經指派給了目標節點。
當客戶端向源節點傳送一個與資料庫鍵有關的命令,並且命令要處理的資料庫鍵恰好就屬於正在被遷移的槽時:源節點會先在自己的資料庫裡查詢指定的鍵,如果找到的話,就直接執行客戶端傳送的命令;相反,如果源節點沒能在自己的資料庫裡找到指定的鍵,那麼這個鍵有可能已經被遷移到目標節點,源節點向客戶端返回一個ASK錯誤,指引客戶端轉向正在匯入槽的目標節點,並再次傳送之前想要執行的命令。
clusterState結構的importing_slots_from 陣列記錄了當前節點正在從其他節點匯入的槽:
typedef struct clusterState{ clusterNode *importing_slots_from[16384]; } clusterState;
如果 importing_slots_from[i]的值不為NULL,而是指向一個clusterNode結構,那麼標識當前節點正在從clusterNode所標識的節點匯入槽i
clusterState結構migrating_slots_to陣列記錄了當前節點正在遷移至其他節點的槽:
typedef struct clusterState{ clusterNode *migratubg_slots_to[16384]; }clusterState;
如果migrating_slots_to[i]的值不為NULL,而是指向一個clusterNode結構,那麼表示當前節點正在將槽i遷移至clusterNode所標識的節點。
ASK錯誤與MOVED錯誤的區別:
MOVED錯誤代表槽的負責權已經從一個節點轉移到另一個節點:在客戶端收到槽i的MOVED錯誤之後,客戶端每次遇到關於槽i的命令請求時,都可以直接將命令請求傳送至MOVED錯誤所指向的節點,因為該節點就是目前負責槽i的節點。
ASK錯誤只是兩個節點在遷移槽的過程中使用的一種臨時措施。ASK錯誤的轉向不會對客戶端今後傳送關於槽i的命令請求產生任何影響,客戶端仍然會將關於槽i的命令請求傳送至目前負責處理槽i的節點。
每天學一點,總會有收穫。
說明:尊重作者智慧財產權,文中內容參考《Redis設計與實現》,僅在此做學習與大家分享。