【Redis入門】-叢集(手動搭建)
使用哨兵模式可以有效的增加資料庫容量,同時可以實現自動化,但是,即使使用哨兵模式,redis叢集的每個資料庫仍然儲存著叢集中的所有資料,這樣就會存在木桶效應:資料庫的總容量受限於儲存記憶體最小的redis節點!
而這裡講的叢集,是對資料庫進行水平擴容,每個節點會儲存不同區域的資料。哨兵和叢集式兩個獨立的功能,但從效能上來看哨兵屬於叢集的子集,當不需要資料分片或者已經在客戶端進行分片的場景下哨兵就足夠了,但是如果需要水平擴容,則就需要創立叢集。(此段摘抄自《Redis入門指南》)。
redis叢集(區別於廣義的叢集)是redis提供的分散式資料庫方案,叢集通過分片(sharding)來進行資料共享
首先,叢集是由多個節點組成的,在沒建立叢集的時候,每個節點都是獨立的,他們處於一個只有自己的叢集中,只有讓各個節點相互連線起來,才能構成一個包含多個節點的叢集。下面開始搭建一個叢集。
步驟一:
我以6382、6383、6384三個節點作為例子,讓這三個節點組成一個叢集。首先,我們需要修改redis配置檔案,開啟節點叢集功能,在redis配置檔案中,有一個cluster-enabled開關,它預設是no,我們設定成yes,這樣,開啟節點之後他本身就處於他自己的叢集中,檢視一個節點是否已經開啟叢集使用命令 info cluster ,如果叢集被正常開啟了,會顯示下圖所示的資訊:
步驟二:
現在,開啟了三個節點之後,實際上就存在了三個完全獨立的叢集,第二步就是把這三個節點連結起來。這個工作使用叢集命令 cluster meet <ip> <port> ,這個命令可以讓當前節點與目標節點進行“握手”,“握手”成功之後,目標節點就加入到了本節點所在的叢集當中。如下圖所示:
握手成功會返回 OK 的資訊。這樣這三個節點已經處於 6382節點的叢集當中,我們使用叢集命令 cluster info 檢視叢集資訊:
有兩個非常重要的地方需要說明:
1. 叢集的資料結構
首先,叢集搭建完成之後,每個節點都會有兩種不同的結構的資料:
1.1. clusterNode 結構,clusterNode結構儲存的是節點本身以及處於叢集中的所有節點的狀態,節點會為自己建立一個clusterNode結構資料,同時,也會為叢集中的所有節點分別建立一個clusterNode結構。clusterNode結構如下圖所示:
這裡已經對部分重要的資訊寫了說明,其中slots 與 numslots屬性是後文為節點分配槽的時候需要使用的,後面會進行講解。需要注意的是link屬性,這個link屬性是一個clusterLink結構,她記錄了連結節點所需的有關資訊,比如套接字描述符、輸入緩衝區、輸出緩衝區等。
1.2. 每個節點還有一個結構是clusterState,它儲存的是當前節點的視角下,叢集目前所處於的狀態,比如多少個節點、線上或者下線等,其結構如下圖:
其中 *slots是後文為節點分配槽的時候需要使用的,後面會進行講解。
2. CLUSTER MEET 命令
這個命令實現了節點之間建立叢集的動作,它可以讓客戶端節點將另一個節點新增到自己的叢集中。其內部的實現步驟是:
假設節點A接受命令,新增節點B
2.1. 首先,節點A會為節點B建立一個clusterNode結構體,並將這個結構新增到clusterState結構中nodes屬性的字典裡面,這一步相當是在A中對節點B進行註冊。
2.2. 然後,開始握手:
節點A會向節點B傳送MEET訊息,此時節點B會為A建立一個clusterNode結構,並將其註冊到自己的clusterState中nodes屬性中。
節點B完成對A的註冊之後,會反饋給A一個PONG。
如果此時節點A接收到了B的反饋(PONG),那麼節點A會再向B傳送一個PING。
如果B接受到了A的PING,那麼握手完成。
2.3. 握手一旦完成,就說明A已經將B視為自己叢集中的節點了,但是還需要得到叢集中其他節點的“認可”,因此,節點A會將B的資訊通過“Gossip”協議傳播給叢集中的其他節點,它其他節點也與節點B進行握手,最終,節點B成功加入到A所在的叢集中。
步驟三:
至此,這個簡單的叢集已經搭建完成,那麼我們嘗試著輸入一些命令,系統會返回給你一個錯誤提示:(error) CLUSTERDOWN The cluster is down,怎麼回事呢,這是因為即使我們搭建好了叢集,但是這個叢集實際上還是處於停止狀態,我們可以是用cluster info 命令檢視一下叢集的狀態,我們可以看到第一項就是 cluster_state:fail ,這表明我們的叢集還是處於關閉的狀態,那麼怎樣才能開啟這個叢集讓它工作呢?這就是接下來我們的工作:為叢集中的節點分配槽!
Redis叢集是通過分片的方式儲存資料庫中的鍵值對的,叢集的整個資料庫被分為16384個槽,我們的資料全部分配在這寫槽當中,而 接下來,我們就是為不同的節點分配不同片段的槽。資料庫中的每個節點都處於0~16383槽中。那麼叢集怎麼算是開啟了呢,那就是這16384個槽都有節點在處理的時候,叢集就會處於線上狀態,反之,只要有一個槽未被處理,那麼叢集就不會工作!
那麼我們如何為節點分配槽呢?我們使用叢集命令 cluster addslots <slot> [slot ...],這個命令可以將一個或多個槽分配給指定的節點,準確的說,它是吧一組hash slots分配給接受到命令的節點,如果命令執行成功,節點將指定的 hash slots 對映到自身,節點將獲得指定的hash slots,同時開始向叢集廣播新的配置。例如:
這個命令需要注意:
1. 這個命令只能作用那些還未分配的槽,如果某個槽已經被分配了,那麼執行這個命令就會報錯;
2. 執行這個命令有一個副作用,如果slot作為其中一個引數設定為importing
,一旦節點向自己分配該slot(以前未繫結)這個狀態將會被清除;
3. 注意一旦一個節點為自己分配了一個slot集合,它就會開始將這個資訊在心跳包的頭裡傳播出去。然而其他節點只有在他們有slot沒有被其他節點繫結或者傳播的新的hash slot的配置年代大於列表中的節點時才會接受這個資訊。這意味著這個命令應該僅通過redis叢集應用管理客戶端例如redsi-trib謹慎使用,而且這個命令如果使用了錯誤的上下文會導致叢集處於錯誤的狀態或者導致資料丟失。
接下來我們就可以為叢集中的節點分配槽了,這裡,我嘗試使用書上介紹的方法一次分配多個槽,使用“...”,但是提示我失敗,如下圖所示:
於是我嘗試使用for迴圈一個一個分配,還是失敗了,於是我用java連結虛擬機器,用java寫for迴圈
private static final String host = "192.168.45.128";
private static final int post = 6383;
private static ArticalService articalService;
public static void main(String[] args) {
Jedis jedis = JedisTools.getJedis(host, post);
articalService = new ArticalService();
System.out.println(jedis.ping());
for(int i=10919;i<10920;i++) {
System.out.println(jedis.clusterAddSlots(i));
}
System.out.println("結束");
JedisTools.releaseJedis(jedis);
}
最終終於將16384個槽分配給了三個節點。節點分配完成之後,使用cluster info命令檢視叢集狀態:
如圖所示,cluster_state已經變成OK,這表明你的叢集已經處於工作狀態!我們可以使用 cluster slots 命令檢視每個節點的槽指派資訊。
接下來,我們來看一下叢集中節點是如果儲存槽的資訊的:
記錄節點的槽指派資訊(clusterNode.slots[16384]和clusterNode.numslots):
我們看上文的clusterNodes結構中的資料,其中有兩個屬性我們還沒有講,那就是slots陣列和numslots。
其中slots陣列記錄著在此節點視角下每個槽的狀態,如果是自己負責的槽,那麼值是1,如果不是自己負責,那值就是0;
而numslots則記錄了自己一共負責了多少個槽,也就是slots陣列中值是1的個數。
記錄叢集的槽指派資訊(clusterState.*slots[16384]):
上文的clusterState結構中也有一個屬性我們沒有介紹,那就是 *slots[16384]指標陣列,這個陣列也記錄了叢集中的16384個槽,不過不同於clusterNode結構中的slots[16384]陣列,clusterState中的slots陣列是指標型別的,每個小格指向了當前槽所被負責的節點的clusterNode結構體。
舉個例子,在*slots[16384]中,slots[5000]這個小格子代表了第5000槽,假設節點6384負責這個槽,那麼slots[5000]就指向了本節點中記錄6384節點資訊的clusterNode結構;如果是NULL,則表明槽5000目前還沒有被分配。
那麼既然clusterNode結構和clusterState結構都記錄了槽的分配狀態,是否是一種多餘的機制呢?事實證明,如果僅僅使用clusterNode結構或者clusterState結構來記錄槽的指派資訊,將無法高效的完成一些問題:
1. 如果僅僅使用clusterNode.slots記錄槽的指派資訊,我們要想知道 第n 個槽是否已經被指派或者指派給了哪一個節點的時候,我們必須遍歷clusterState.nodes陣列(該節點記錄的其他節點的資訊),檢查所有的clusterNode結構中的slots陣列,直到找到了第n槽被誰負責,或者未被指派,這個過程的時間複雜度是O(N),N是叢集中的節點數量。
而如果使用clusterState.*slots[],則時間複雜度是O(1)。
2. 僅僅使用clusterState.*slots[]來記錄槽指派資訊,這裡需要說明一點,每次為節點新增槽的時候,節點都會將自己負責的槽(也就是clusterNode.slots陣列)傳送給其他節點以便更新狀態。若僅僅使用clusterState,那麼每次需要傳送本節點的槽指派資訊的時候,我們都需要整個clusterState.*slots[]陣列,記錄哪些槽是本節點負責的,然後在傳送出去。這樣比起clusterNode.slots結構,低效又麻煩。
接下來,我們來看看 cluster addslots 命令內部的實現過程:
執行:A節點 cluster addslots 1 2
1. 一旦執行了這個命令,該節點的clusterState.*slots[]陣列的所以1和索引2就會指向本節點的clusterNode結構;同時,sluster.Node.slots[]中的索引1和索引2的位置變成了1。
2. 接下來,廣播槽指派資訊,也就是將自己最新的槽指派資訊傳送給其他節點。傳送的就是本節點的clusterNode.slots[]陣列。其他節點(B節點)在接收到這個訊息之後,就會更新B節點本身用來記錄A節點資訊的clusterNode結構。
至此,我們已經講完了關於叢集建立的所有步驟,接下來我們新增一些資料進去吧!
我現在在6382節點輸入一個數據:
好奇怪,為什麼會出錯呢,看提示資訊:系統提示了轉向另一個redis節點。原來,不同的鍵會被分配給不同的槽中,如果我們在傳送與資料鍵有關的命令時,接收命令的節點會首先計算出要處理的鍵屬於哪一個槽,如果自己負責這個槽,那麼命令將會被處理,如果不是本節點處理的槽,就會返回MOVE錯誤,並將正確的節點資訊返回回去。
我們就拿上圖的例子說明:
1. 首先,6382節點接收命令:set k1 v1,那麼6382節點先計算這個K1屬於哪一個槽,我們可以使用叢集命令:cluster keyslot <key> 來檢視鍵屬於哪一個槽。
2. 這裡name的槽是5796,然後,6382節點會查詢自己的clusterState.*[16384]陣列,也就是slots[5796],如果它指向clusterState.myself,則說明是自己負責的,那就回執行命令;但是這裡6382節點值負責到5460節點,所以返回MOVE錯誤,並將slots[5796]指向的clusterNode節點的IP和埠號反饋給客戶端。
我們可以在6383節點嘗試再次執行:
可以看到,這次成功了。
注意:叢集模式的redis-cli 客戶端在接收MOVE錯誤的時候,會自動轉向到正確的節點!如果向使用叢集模式的redis-cli,在連結redis服務的時候使用 -c ,例如:
redis-cli -c -p 6382
那麼就進入叢集模式,該模式下MOVED錯誤會自動轉向: