1. 程式人生 > 實用技巧 >《Redis設計與實現》筆記3—多機資料庫的實現

《Redis設計與實現》筆記3—多機資料庫的實現

一、複製

在Redis中,使用者可以通過執行SLAVEOF命令或設定slaveof選項,讓一個伺服器去複製另外一個伺服器,被複制的伺服器稱為主伺服器,進行復制的伺服器被稱為從伺服器

1、舊版複製功能的實現

Redis的複製功能分為同步和命令傳播兩個操作:

  • 同步操作用於將從伺服器的資料庫狀態更新至主伺服器當前所處的資料庫狀態;
  • 命令傳播操作則用於在主伺服器的資料庫狀態被修改,導致從伺服器的資料庫狀態不一致時,讓主伺服器的資料庫重新回到一致

1、同步

當客戶端從伺服器傳送SLAVEOF命令時,從伺服器首先會執行同步操作,它會向主伺服器傳送SYNC命令,步驟如下:

  • 從伺服器向主伺服器傳送SYNC命令;
  • 收到SYNC命令後主伺服器執行BGSAVE命令,在後臺生成一個RDB檔案,使用一個緩衝區記錄從現在開始的所有寫命令;
  • 主伺服器的BGSAVE執行完成後,主伺服器將RDB檔案傳送給從伺服器,從伺服器接受並載入RDB檔案,將自己的狀態更新至RDB檔案記錄的狀態;
  • 主伺服器將記錄在緩衝區的寫命令傳送給從伺服器,從伺服器執行這些寫命令,將自身狀態更新至主伺服器當前的狀態;

2、命令傳播

當同步操作執行完畢之後,主從伺服器的資料庫達到一致的狀態,但當主伺服器執行寫操作後,主從伺服器狀態將不再一致;為了確保狀態的一致,主伺服器會將接受到的寫命令傳送給從伺服器,執行完成後狀態再次回到一致

2、舊版複製功能的缺陷

Redis2.8版本之前,從伺服器的複製分為兩種情況:

  • 初次複製:之前沒有複製過主伺服器,或者複製的主伺服器與上一次主伺服器不同;
  • 斷線後重新複製:處於命令傳播的主從伺服器因網路原因中斷了複製,但從伺服器重新連上了主伺服器,並繼續複製

後一種情況雖然在功能上沒有問題,但是效率卻非常低,因為它會在重新連線後重新發送SYNC命令接受RDB檔案,但此時的RDB檔案已經包含了部分之前複製過的資料,因而伺服器做了部分重複的操作

3、新版複製功能的實現

Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操作,PSYNC命令具有完整重同步和部分重同步兩種模式:

完整重同步用於處理初次複製的情況,其執行步驟與SYNC命令的執行步驟基本一致;

部分重同步用於處理斷線後重複製情況,從伺服器斷線後,如果條件允許,主伺服器可以將從斷線開始執行的寫命令傳送給從伺服器,從伺服器接受並執行這些命令後,就可以將資料庫更新至主伺服器當前所處的狀態

4、部分重同步的實現

4.1、複製偏移量

執行復制的雙方,會分別維護一個複製偏移量。主伺服器每次向從伺服器傳播N個位元組的資料時,就將自己的複製偏移量的值加N,從伺服器每次接收到主伺服器傳播來的N個位元組的資料,就將自己的複製偏移量的值加N

通過對比主從伺服器的複製偏移量,可續可以判斷主從伺服器的狀態是否一致

4.2、複製積壓緩衝區

複製積壓緩衝區是主伺服器維護的一個固定長度先進先出的而佇列,預設大小為1MB(固定長度是指隊首元素被彈出後隊尾元素被入隊);主伺服器進行命令傳播時,不僅會將命令傳送給從伺服器,還會將命令寫入複製積壓緩衝區內;因此主伺服器的複製積壓緩衝區會儲存著一部分最近傳播的寫命令,並且複製積壓緩衝區會為佇列中的每個位元組記錄相應的複製偏移量

當從伺服器重新連線上主伺服器之後,會將自己的複製偏移量傳送給主伺服器:

  • 如果偏移量之後的資料存在於複製積壓緩衝區,那麼主從伺服器將執行部分重同步操作;
  • 如果偏移量之後的資料不存在於複製積壓緩衝區,那麼主從伺服器將執行完整重同步操作;

當然我們可以根據公式second*write_size_per_second來估算複製積壓緩衝區的大小,為了安全起見也可以設定為該結果的2倍,來應對大部分的斷線情況。複製積壓緩衝區大小的修改方法,可以參考配置檔案中關於repl_backlog_size選項

4.3、伺服器執行ID

除了上述兩項外,部分重同步還需要用到伺服器執行ID;每個Redis伺服器都會有自己的執行ID,每個執行ID在伺服器啟動時自動生成,由40個隨機的十六進位制字元組成

當從伺服器對主伺服器進行初次複製時,主伺服器會將自己的執行ID傳送給從伺服器,從伺服器會將這個執行ID儲存起來,當從伺服器斷線並重新連線上一個主伺服器後,會向當前連線的主伺服器傳送之前儲存的執行ID;如果從伺服器儲存的執行ID和當前連線的主伺服器執行ID相同,那麼斷線前複製的就是當前的主伺服器,主伺服器惡意繼續嘗試執行部分重同步操作;如果ID不同,將執行完整重同步操作

5、PSYNC命令的實現

1、PSYNC命令的呼叫方法有兩種:

  • 如果伺服器以前沒有複製過任何主伺服器,或者之前執行過SLAVEOF no one命令(從伺服器關閉複製功能),那麼從伺服器將傳送PSYNC ? -1命令,主動請求伺服器進行完整重同步;
  • 如果從伺服器已經複製過主伺服器,那麼從伺服器將傳送PSYNC 命令,其中runid是上一次複製的主伺服器的執行Id,而offset則是從伺服器當前的複製偏移量

接收到PSYNC命令的主伺服器會返回以下三種回覆的一種:

  • 主伺服器返回+FULLERSYNC 回覆,表示主伺服器將與從伺服器執行完整同步操作;
  • 主伺服器返回+CONTINUE,表示主伺服器將與從伺服器執行部分重同步操作,從伺服器等著主伺服器傳送自己缺少的資料即可;
  • 主伺服器返回-ERR,表示主伺服器的版本低於Redis2.8,無法識別PSYNC命令,從伺服器將向主伺服器傳送SYNC命令,並執行完整同步操作;

6、複製的實現

  1. 設定主伺服器的地址和埠:從伺服器首先要做的通過SLAVEOF命令將客戶端給定的主伺服器IP地址和埠儲存到伺服器狀態的masterhost屬性和masterport屬性中,完成後客戶端返回OK,複製工作也將在返回OK之後進行;
  2. 建立套接字連線:在SLAVEOF命令執行之後,從伺服器將根據命令設定的IP地址和埠建立連向主伺服器的套接字連線,連線成功後,從伺服器將為這個套接字關聯一個專門用於處理複製工作的檔案事件處理器,該處理器將負責執行後續的複製工作,如接受RDB檔案,接受主伺服器傳播的寫命令等。此時從伺服器可以看作是一個連線到主伺服器的客戶端;
  3. 傳送Ping命令:從伺服器成為主伺服器的客戶端之後,第一件事將傳送Ping命令:一是為了檢查套接字的讀寫是否正常,二是為了檢查主伺服器能否正常處理命令請求。傳送ping命令將會得到以下三種回覆的一種:①主伺服器返回命令回覆,但從伺服器不能在規定時間內讀取出回覆內容,表示網路連線狀態不佳,從伺服器將斷開連線並重新建立套接字;②返回一個錯誤,表示主伺服器暫時無法處理從伺服器的命令請求,從伺服器將斷開連線並重新建立套接字;③從伺服器讀取到“PONG”回覆,表示網路連線正常,從伺服器將繼續執行接下來的步驟
  4. 身份驗證:伺服器收到“PONG”回覆後,將判斷從伺服器是否設定了masterauth選項,如果設定了該選項則需要進行身份驗證,反之則不需要進行身份驗證。如果需要進行身份驗證,從伺服器將傳送一條AUTH命令,命令引數為從伺服器masterauth選項的值。從伺服器在身份驗證階段可能遇到以下情況:①主伺服器沒有設定requirepass選項,從伺服器也沒有設定masterauth選項,複製工作將繼續進行;②從伺服器通過AUTH傳送的密碼和主伺服器requirepass選項設定的密碼相同,複製工作繼續進行,密碼不同則返回invalid password錯誤;③主伺服器設定了requirepass選項,但從伺服器沒有設定masterauth選項,主伺服器將返回NOAuth錯誤;而如果主伺服器沒有設定requirepass選項,但從伺服器設定了masterauth選項,那麼主伺服器將返回no password is set錯誤。一旦出現錯誤的情況,從伺服器將中止目前的複製工作,並從建立套接字開始重複執行復制,直到身份驗證通過,或從伺服器放棄複製。總結如下:
  5. 傳送埠資訊:身份驗證通過後,從伺服器將執行ReplConf listening-port 命令,向主伺服器傳送從伺服器的監聽埠號,主伺服器在接收到這個命令之後,會將埠號記錄在從伺服器所對應的客戶端狀態的slave_listening_port屬性中
  6. 同步:從伺服器將向主伺服器傳送PSYNC命令,執行同步操作,並將自己的資料更新至主伺服器資料庫當前所處的狀態。執行同步操作前,從伺服器是主伺服器的客戶端,執行同步操作之後,主伺服器也將成為從伺服器的客戶端。在完整同步操作時,主伺服器需要成為從伺服器的客戶端,才能將儲存在緩衝區的寫命令傳送給從伺服器;而在執行部分重同步操作時,主伺服器需要成為從伺服器的客戶端,才能將儲存在複製積壓緩衝區的寫命令傳送給從伺服器。不僅同步操作需要利用到這一點,主伺服器對從伺服器執行命令傳播時也需要藉助這一點
  7. 命令的傳播:完成同步操作後,主伺服器將進入命令傳播階段,從伺服器只需要一直接受並執行主伺服器傳送的寫命令即可保持狀態的一致

7、心跳檢測

在命令傳播階段,從伺服器預設以每秒一次的頻率,向主伺服器傳送命令:ReplConf ACK <replication_offset>,其中replication_offset是從伺服器當前的複製偏移量;傳送命令的目的如下:①檢測主從伺服器的網路連線狀態;②輔助實現min-salves選項;③檢測命令丟失

  1. 主從伺服器可以通過傳送或接受ReplConf ACK命令來檢查兩者之間的網路連線是否正常,服務主伺服器超過1秒沒有接收到從伺服器傳送的ReplConf ACK命令,即表示主從伺服器之間的連接出現問題。
  2. Redis的min-slaves-to-write和min-slaves-max-lag選項可以防止主伺服器在不安全的情況下執行寫命令;如min-slaves-to-write設定為3,min-slaves-max-lag設定為10,即表示從伺服器的數量小於3或者3個從伺服器的延遲值都大於等於10秒時,主伺服器將拒絕執行寫命令。
  3. 若因網路原因導致主伺服器傳播給從伺服器的寫命令丟失,那麼當從伺服器傳送ReplConf ACK命令時,主伺服器將發現從伺服器的複製偏移量少於自己的複製偏移量,這是主伺服器將會根據從伺服器提交的複製偏移量,複製在積壓緩衝器中從伺服器缺少的資料,傳送給從伺服器。

主伺服器補發缺失資料的操作與部分重同步操作非常相似,區別在於補發缺失資料的操作是在沒有斷線的情況下執行的,而部分重同步操作是在斷線重連後進行的

二、Sentinel(哨兵)

Sentinel是Redis的高可用性解決方案:通過一個或多個Sentinel例項組成的Sentinel系統,可以監視任意多個主伺服器,以及這些主服務屬下的所有從伺服器,並在被監視的主伺服器進入下線狀態時,自動將下線主服務屬下的某個從伺服器升級為主伺服器,代替已下線的主伺服器繼續處理命令請求(故障轉移操作)。如果之前下線的主伺服器重新上線,它將被Sentinel系統降級為所屬組的從伺服器

1、啟動並初始化Sentinel

啟動一個Sentinel可以使用redis-sentinel /path/to/your/sentinel.confredis-server /path/to/your/sentinel.conf --sentinel命令,它們的效果完全一致。而當Sentinel啟動時,它將執行以下步驟:①初始化伺服器;②將普通Redis伺服器使用的程式碼替換成Sentinel專用程式碼;③初始化Sentinel狀態;④根據給定的配置檔案,初始化Sentinel的監視主伺服器列表;⑤建立連向主伺服器的網路連線;

1、初始化伺服器

Sentinel本質上就是一個執行在特殊模式下的Redis伺服器,所以啟動的第一步就是就是初始化一個普通的Redis伺服器。但因Sentinel執行的工作和普通Redis伺服器執行的工作不同,所以初始化過程與普通伺服器有所區別,如下表在Sentinel模式下,伺服器主要功能的使用情況:

2、使用Redis專用程式碼

該步驟會將一部分普通Redis伺服器使用的程式碼替換成Sentinel專用程式碼,如普通Redis伺服器使用redis.c/redisCommandTable作為伺服器的命令表,而Sentinel則使用sentinel.c/sentinelcmds作為伺服器的命令表,這也是部分命令無法在Sentinel模式下執行的原因

3、初始化Sentinel狀態

伺服器會初始化一個sentinel.c/sentinelSatae結構,該結構儲存了伺服器中所有和Sentinel功能有關的狀態(一般狀態仍由redis.c/redisServer結構儲存),如下圖:

4、初始化Sentinel狀態的masters屬性

Sentinel狀態中的masters字典記錄了所有被Sentinel監視的主伺服器的相關資訊:鍵是被監視的主伺服器的名字;值則是被監視的主伺服器對應的sentinel.c/sentinelRedisInstance結構。

每個sentinelRedisInstance結構代表一個被Sebtinel監視的Redis伺服器例項,這個例項可以是主伺服器、從伺服器或者是另外一個Sentinel。此結構例項包含的屬性非常多,以下為其中的一部分:

sentinelRedisInstance.addr屬性是一個指向sentinel.c/sentinelAddr結構的指標,這個結構儲存著例項的IP地址和埠號;

對Sentinel狀態的初始化將引發master字典的初始化,而master字典的初始化是根據被載入的Sentinel配置檔案來進行的;

5、建立連向主伺服器的網路連線

連線完成後,Sentinel將成為主伺服器的客戶端,它可以向主伺服器傳送命令並獲取回覆資訊;對於每個被Sentinel監視的主服務來說,Sentinel會建立兩個連向主伺服器的非同步網路連線(主要原因是由於目前的釋出訂閱功能中,被髮送的資訊不會儲存在Redis伺服器中):

  • 一個是命令連線,這個連線專門用於向主伺服器傳送命令,並接受命令回覆;
  • 另一個是訂閱連線,這個連線專門用於訂閱主伺服器的_sentinel_:hello頻道

2、獲取主伺服器的資訊

1、Sentinel預設會以每10秒一次的頻率,通過命令連線向被監視的主伺服器傳送INFO命令,並通過分析INFO命令的回覆來獲取主伺服器的當前資訊。通過分析主伺服器返回的INFO命令回覆,Sentinel可以獲取以下兩方面的資訊:

  • 主伺服器本身的資訊,包括run_id域記錄的伺服器執行ID,以及role域記錄的伺服器角色;
  • 主伺服器下所有從伺服器的資訊,每個從伺服器都由一個"slave"字串開頭的行記錄,包括從伺服器的IP地址和埠號資訊,通過這些資訊,Sentinel無需使用者提供的從伺服器地址資訊,就可以自動發現從伺服器;

2、根據run_id域和role域記錄的資訊,Sentinel將對主伺服器的例項結構進行更新,如主伺服器重啟後run_id變更,Sentinel檢測到這類情況後,將會對例項結構的執行ID進行更新;從伺服器資訊將被用於更新主伺服器例項結構的salves字典,該字典記錄了主伺服器下的從伺服器名單:

  • 字典的值是由Sentinel自動設定的從伺服器名字,即ip:port;
  • 字典的值是從伺服器對應的例項結構

3、Sentinel在分析INFO命令中包含的從伺服器資訊時,會檢查從伺服器對應的例項結構是否已經存在於slaves字典中:

  • 如果已存在,Sentinel將會對從伺服器的例項結構進行更新;
  • 如果不存在,則代表從伺服器是新發現的伺服器,Sentinel將為這個從伺服器建立一個例項結構;

如上圖,注意flags屬性值和name屬性,主伺服器的name是使用者使用Sentinel配置檔案設定的,從伺服器的name是Sentinel根據IP地址和埠號自動設定的

3、獲取從伺服器資訊

當Sentinel發現主伺服器有新的從伺服器出現時,除了會為這個新的從伺服器建立相應的例項結構外,Sentinel還會建立連線到從伺服器的命令連線和訂閱連線。

建立命令連線之後,Sentinel在預設情況下,會以每十秒一次的頻率通過命令連線向從伺服器傳送INFO命令,並得到相關的回覆,根據回覆內容Sentinel會提取出以下資訊:①從伺服器的run_id;②從伺服器的role;③主伺服器的IP地址和埠號;④主從伺服器的連線狀態;⑤從伺服器的優先順序;⑥從伺服器的複製偏移量。根據這些資訊,Sentinel會對從伺服器的例項結構進行更新

4、向主伺服器和從伺服器傳送資訊

在預設情況下,Sentinel會以每2秒一次的頻率,通過命令連線向所有被監視的主伺服器和從伺服器傳送以下命令:

該命令向伺服器的_Sentinel_:hello頻道傳送了一條資訊,s_開頭的引數記錄的是Sentinel本身的資訊;m_開頭的引數記錄的是主伺服器的資訊,如果Sentinel正在監視的是主伺服器,那麼這些引數記錄的就是主伺服器的資訊,如果Sentinel正在監視的是從伺服器,那麼這些引數記錄的就是從伺服器正在複製的主伺服器的資訊。詳細如下:

5、接受來自主伺服器和從伺服器的頻道資訊

當Sentinel與一個主伺服器或從伺服器建立起訂閱連線之後,Sentinel就會通過訂閱連線,向伺服器傳送以下命令:Subscribe _sentinel_:hello,而Sentinel對_Sentinel_:hello頻道的訂閱會一直持續到Sentinel與伺服器的連線斷開為止。也就是說,每個與Sentinel連線的伺服器,Sentinel既通過命令連線向伺服器的_Sentinel_:hello頻道傳送資訊,又通過訂閱連線從伺服器的_Sentinel_:hello頻道接受資訊,如下圖:

對於監視同一個伺服器的多個Sentinel來說,一個Sentinel傳送的訊息會被其他Sentinel接受到,這些資訊會用於更新其他Sentinel對傳送訊息的Sentinel的認知,也會被用於更新其他Sentinel對被監視伺服器的認知,舉例說明如下:

1、更新sentinels字典

Sentinel為主伺服器建立的例項結構中的Sentinel字典儲存了除Sentinel本身之外,所有同樣監視這個主伺服器的其他Sentinel資料:

  • sentinels字典中的鍵是一個Sentinel的名字,格式為ip:port;
  • sentinels字典中的值則是鍵所對應Sentinel的例項結構

①當一個Sentinel接收到其他Sentinel發來的訊息時,它會從資訊中分析並提取出以下兩方面的引數:

  • 與Sentinel有關的引數:傳送訊息的Sentinel的IP地址、埠號、執行ID和配置紀元;
  • 與主伺服器有關的引數:傳送訊息的Sentinel正在監視的主伺服器的名字、IP地址、埠號和配置紀元;

②接受訊息的Sentinel首先會提取出資訊中的主伺服器引數,並在自己的Sentinel狀態的masters字典中查詢對應的主伺服器結構,確認主伺服器例項結構的sentinel字典中,是否存在提取出的主伺服器,如果存在則更新資訊,如果不存在則會新增。

2、建立連向其他Sentinel的命令連線

當Sentinel通過頻道資訊發現一個新的Sentinel時,它不僅會為新Sentinel在sentinels字典中建立新的例項結構,還會建立一個連向新Sentinel的命令連線,而新Sentinel同樣也會建立連向這個Sentinel的命令連線,最終監視同一伺服器的Sentinel形成相互連線的網路。(Sentinel需要通過主伺服器或從伺服器發來的頻道資訊來發現未知的新Sentinel,所以需要和它們建立訂閱連線,而Sentinel之間只需要通過命令連線進行通訊)

6、監測主觀下線狀態

預設情況下,Sentinel會以每秒一次的頻率向所有與他建立了命令連線的例項(包括主伺服器、從伺服器、其他Sentinel)傳送PING命令,並通過例項返回的PING命令回覆來判斷例項是否線上。PING命令的回覆可以分為以下兩種情況:

  • 有效回覆:例項返回+PONG、-LOADING、-MASTERDOWN三種回覆的一種;
  • 無效回覆:例項返回+PONG、-LOANDING、-MASTERDOWN三種回覆外的其他回覆,或者在指定時間內沒有返回回覆

Sentinel配置檔案中的down-after-milliseconds選項指定了Sentinel判斷例項進入主觀下線所需要的時間長度;如果在這個時間長度內連續返回無效回覆,那麼Sentinel會將這個例項結構flags屬性標識為SRI_S_DOWN,來表示該例項已進入主觀下線狀態(注意這裡使用者配置的down-after-milliseconds同樣還會用來判斷master屬下所有從伺服器和同樣監視master的Sentinel是否進入主觀下線狀態;另外不同的Sentinel可以設定不同的主觀下線時長)

7、檢查客觀下線狀態

當Sentinel將一個主伺服器判斷為主觀下線後,它會向其他監視這個主伺服器的Sentinel進行詢問,看它們是否也已確認該主伺服器進入下線狀態(可以是主觀下線也可以是客觀下線),當Sentinel從其他的Sentinel接收到足夠數量的已下線判斷後,就會判定從伺服器為客觀下線,並對主伺服器進行故障轉移操作

1、傳送Sentinel is-master-down-by-addr命令

Sentinel會使用Sentinel is-master-down-by-addr <IP> <Port> <current_epoch> <runid>命令來詢問其他Sentinel是否同意主伺服器已下線,命令中的引數說明如下:

2、接受Sentinel is-master-down-by-addr命令

當目標Sentinel接受到源Sentinel傳送的上述命令時,目標Sentinel會分析並取出命令請求中包含的各個引數,並根據IP和Port來檢查主伺服器是否已經下線,然後向源Sentinel傳送包含三個引數的Multi Bulk回覆,即<down_state>,

3、接受Sentinel is-master-down-by-addr命令的回覆

根據其他Sentinel返回的命令回覆,Sentinel將統計其他同意主伺服器已下線的數量,當這一數量達到配置指定的判斷客觀下線所需的數量時,Sentinel會將主伺服器例項結構的flags屬性的SRI_O_Down標識開啟,表示主伺服器已進入客觀下線狀態(需要注意的是不同的Sentinel判斷客觀下線的條件可以不同)

8、選舉領頭Sentinel

當一個主伺服器被判斷為客觀下線後,監視這個主伺服器的各個Sentinel會進行協商,選舉出一個領頭Sentinel,並由領頭Sentinel對下線伺服器進行故障轉移操作。選舉的規則和方法如下:

9、故障轉移

在選舉出領頭Sentinel後,領頭Sentinel將對已下線的主伺服器執行故障轉移操作,共三個步驟:

  • 在已下線主伺服器屬下的所有從伺服器裡面,挑選一個從伺服器將其轉換為主伺服器;
  • 讓已下線的主伺服器屬下的所有從伺服器改為複製新的主伺服器;
  • 將已下線的主伺服器設定為新的主伺服器的從伺服器,當其上線時,會成為新伺服器的從伺服器;

1、選出新的主伺服器

故障轉移操作的第一步就是在已下線主伺服器屬下的所有從伺服器中選出一個狀態良好、資料完整的從伺服器,然後向這個從伺服器傳送SLAVEOF no one命令,將這個從伺服器轉換為主伺服器;挑選規則如下:

領頭伺服器向被選中的從伺服器傳送完SLAVEOF no one命令後,領頭Sentinel會以每秒一次的頻率(平時時10秒一次)向被升級的從伺服器傳送INFO命令,並觀察命令回覆中的角色資訊,當被升級伺服器的role從slave變為master時,領頭Sentinel將知曉從伺服器已成功升級為主伺服器

2、修改從伺服器的複製目標

當新的主伺服器出現之後,領頭Sentinel要做的就是讓已下線主伺服器的屬下所有從伺服器去複製新的主伺服器,這一個動作可以通過向從伺服器傳送SLAVEOF命令來實現

3、將舊的主伺服器變為從伺服器

故障轉移的最後一步,就是將已下線的主伺服器設定為新的主伺服器的從伺服器,這種設定時儲存在舊的主伺服器對應的例項結構裡面的,所以當舊的主伺服器上線時,Sentinel就會向它傳送SLAVEOF命令,讓他成為新的主伺服器的從伺服器

三、叢集

Redis叢集是Redis提供的分散式資料庫方案,它通過分片(sharding)來進行資料共享,並提供複製和故障轉移功能

1、節點

一個Redis叢集通常由多個節點組成,剛開始的時候每個節點都是相互獨立的,它們處於一個只包含自己的叢集當中,要組建一個真正可用的工作叢集,我們必須將各個獨立的節點連線起來,構成一個包含多個節點的叢集。連線節點可以用命令Cluster Meet <ip> <port>

向一個節點發送Cluster meet命令,可以讓節點與ip和port所指定的節點進行握手,握手成功後,傳送命令的節點就會將被連線的節點新增到當前的叢集中。完成後可以通過Cluster Nodes命令檢視當前的叢集資訊

1、啟動節點

一個節點就是一個執行在叢集模式下的Redis伺服器,Redis伺服器在啟動時會根據cluster-enabled配置選項是否為yes來決定是否開啟伺服器的叢集模式,否則只會開啟伺服器的單機模式成為一個普通的Redis伺服器

開啟叢集模式的Redis伺服器會繼續使用所有在單機模式中使用的伺服器元件,除此之外叢集模式下用到的資料,節點將它們儲存到三種資料結構中,即cluster.h/clusterNode結構、cluster.h/clusterLink結構和cluster.h/clusterState結構

2、叢集資料結構

①clusterNode結構儲存了一個節點當前的狀態,如節點的建立時間,節點的名字,節點當前的配置紀元、節點的IP地址和埠號等。每個節點都會使用一個clusterNode結構來記錄自己當前的狀態,併為叢集中的其他節點都建立一個相應的clusterNode結構,來記錄節點的狀態,如下:

②其中Link屬性是一個clusterLink結構,該結構儲存了連線節點所需的所有資訊,如套接字描述符、輸入緩衝區和輸出緩衝區,如下:

③每個節點都儲存著一個clusterState結構,這個結構記錄了當前節點的視角下,叢集目前所處的狀態,比如叢集是線上還是下線,叢集所包含的節點數、叢集當前的配置紀元等,如下:

如下是一個ClusterState結構示意圖:

3、Cluster meet命令

通過向節點A傳送Cluster Meet命令,客戶端可以讓接受到命令的節點A將接節點B新增至節點A所在的叢集中。接受到命令的節點A會和節點B進行握手操作,來確認彼此的存在,為進一步的通訊打好基礎:

  1. 節點A會為節點B建立一個clusterNode結構,並將該結構新增到自己的clusterState.nodes字典中;
  2. 節點A根據Cluster meet命令給定的IP和埠號,向節點B傳送一條Meet訊息;
  3. 正常情況下,節點B會接收到節點A傳送的meet訊息,節點B會為節點A建立一個clusterNode結構,並將該結構新增到自己的clusterState.Node字典中;
  4. 節點B向節點A返回一條PONG訊息;
  5. 正常情況下,節點A將接收到節點B返回的PONG訊息,通過PONG訊息節點A可以知道節點B已經成功接受到自己傳送的meet訊息;
  6. 之後節點A會向節點B返回一條PING訊息;
  7. 正常情況下,節點B將接受到節點A返回的PING訊息,通過這條PING訊息,節點B可以知道節點A已經成功接收到了自己返回的PONG訊息,此時握手完成

握手完成後,節點A會將節點B的資訊通過Gossip協議傳播給叢集中的其他節點,讓其他節點也與節點B進行握手,最後節點B會被叢集中的所有節點所認識

2、槽指派

Redis叢集通過分片的方式來儲存資料庫中的鍵值對,叢集的整個資料庫被分為16384個槽(slot),資料庫中的每個鍵值對都屬於這16384個槽中的其中一個,而叢集中的每個節點都可以處理0個或16384個槽。

當資料庫中的16384個槽都有節點在處理時,叢集處於上線模式;相反,只要有一個槽沒有得到處理,那麼叢集將處於下線模式。

可以使用Cluster Info命令來檢視叢集的狀態;通過向節點發送Cluster AddSlots <slot> [slot......]命令,我們可以將一個或多個槽指派給節點負責;

1、記錄節點的槽指派資訊

clusterNode結構的slots屬性和numslot屬性記錄了節點負責處理哪些槽:

  • slots屬性是一個二進位制位陣列,陣列的長度為16284/8=2048個位元組,共16384個二進位制位。Redis從索引0開始到索引16384,對slots陣列中的16384個二進位制位進行編號,並根據索引上二進位制位的值判斷是否負責處理槽,二進位制為1代表負責,為0代表不負責;
  • numslots屬性記錄節點負責處理的槽的數量

2、傳播節點的槽指派資訊

一個節點除了會將自己負責處理的槽記錄在clusterNode結構的slots屬性和numslots屬性外,它還會將自己的slots陣列通過訊息傳送給叢集中的其他節點,來告知其他節點自己目前負責處理哪些槽。

當節點A通過訊息從節點B中接收到節點B的slots陣列時,節點A會在自己的clusterState.nodes字典中查詢節點B對應的clusterNode結構,並對結構中的slots陣列進行儲存或更新。真是由於這種機制,叢集中的每個節點都會知道資料庫中的16384個槽被指派給了哪個節點

3、記錄叢集所有槽的指派資訊

clusterState結構中的slots陣列記錄了叢集中所有16384個槽的指派資訊;slots陣列包含16384個項,每個陣列都是一個指向clusterNode結構的指標。如果指標指向為null則代表槽未指派給任何節點,如果指向一個clusterNode結構則表示槽已經指派給了clusterNode結構所代表的節點。

如果節點只使用clusterNode.slots陣列來記錄槽的指派資訊,那麼為了知道槽位i是否被指派或槽位i被指派給了哪些節點,程式需要遍歷clusterStata.nodes字典中所有的clusterNode結構,檢查這些結構的slots陣列直到找到負責處理槽i的節點,整個過程的複雜度位O(n)。而通過將所有槽的指派資訊儲存在clusterState.slots陣列中,程式要檢查槽位i是否被指派,又或者取得負責處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,而這個複雜度僅為O(1)

雖然clusterState.slots陣列記錄了叢集中所有槽的指派資訊,但使用clusterNode結構的slots陣列來記錄單個節點的槽指派資訊仍是有必要的。因為當程式需要將某個節點的槽指派資訊通過訊息傳送給其他節點時,程式只需要將相應節點的clusterNode.slots陣列傳送出去就可以了;另一方面,如果Redis不使用clusterNode.slots陣列,而單獨使用clusterState.slots陣列的話,每次將節點A的槽指派資訊傳播給其他節點時,程式都要先遍歷整個clusterState.slots陣列,記錄節點A負責處理哪些槽,才能傳送節點A的槽指派資訊。

兩者的主要區別:clusterState.slots陣列記錄了叢集中所有槽的指派資訊,而clusterNode.slots陣列只記錄了clusterNode結構所代表的節點的槽指派資訊

4、Cluster AddSlots命令的實現

Cluster AddSlots命令接受一個或多個槽作為引數,並將所有輸入的槽指派給接受該命令的節點負責,如下:

3、在叢集中執行命令

在對資料庫中的16384個槽都進行了指派之後,叢集就會進入上線狀態。當客戶端向節點發送與資料庫鍵有關的命令時,接受命令的節點會計算出處理命令的資料庫鍵屬於哪個槽,並檢查這個槽是否指派給了自己。如果鍵所在的槽指派給了當前節點,那麼節點將直接執行這個命令;如果鍵所在的槽沒有指派當前節點,當前節點會向客戶端返回一個MOVED錯誤,指引客戶端轉向至正確的節點,並再次傳送之前要執行的命令。

1、計算鍵屬於哪個槽

節點使用以下演算法來判斷給定的key屬於哪個槽:def slot_number(key) : return CRC16(key) & 16384;其中CRC16(key)語句用於計算鍵的CRC-16校驗和,而 & 16384語句則用於計算介於0~16384之間的整數作為鍵的槽號

Cluster keySlot命令就是基於上述的分配演算法,可以用來檢視一個給定的鍵屬於哪個槽

2、判斷槽是否由當前節點負責處理

當節點計算出鍵所屬的槽後,節點就會檢查自己在clusterState.slots陣列中的項i,判斷鍵所在的槽是否由自己負責。如果clusterState.Slots[i]等於clusterState.myself,即說明槽i由當前節點負責,節點可以執行客戶端傳送的命令;如果不相等,節點會根據clusterState.Slots[i]指向的clusterNode結構,並根據結構中所記錄的節點IP和埠號,向客戶端返回MOVED錯誤,指引客戶端轉向至正在處理槽i的節點

3、MOVED錯誤

當節點發現鍵所在的槽不由自己負責時,節點就會向客戶端返回一個MOVED錯誤。MOVED錯誤的格式為MOVED <slot> <ip>:<port>,其中slot為鍵所在的槽,而IP和Port則是負責處理槽slot的節點的IP地址和埠號。如錯誤Moved 10086 127.0.0.1:7002表示槽10086正在由IP為127.0.0.1埠為7002的節點負責。

當客戶端接收到節點返回的MOVED錯誤時,客戶端會根據MOVED錯誤中提供的IP地址和埠號,轉向至負責處理槽Slot的節點,並向該節點重新發送之前想要執行的命令。一個叢集客戶端通常會與叢集中的多個節點建立套接字連線,而所謂的節點轉向實際上就是換一個套接字來發送命令。如果客戶端尚未與想要轉向的節點建立套接字連線,那麼客戶端先根據MOVED錯誤提供的IP地址和埠號來連線節點,然後再進行轉向。

需要注意的是,叢集模式的redis-cli客戶端在接收到MOVED錯誤時,並不會打印出MOVED錯誤,而是根據MOVED錯誤自動進行節點的轉向,並自動打印出轉向資訊。而單機模式下的redis-cli客戶端則會將MOVED錯誤打印出來

4、節點資料庫的實現

叢集節點儲存鍵值對以及鍵值對過期時間的方式,與單機Redis伺服器的機制完全相同,唯一的區別是節點只能使用0號資料庫,而單機Redis伺服器則沒有這一限制。除了將鍵值對儲存在資料庫之外,節點還會用clusterState結構中的slots_to_keys跳躍表來儲存槽和鍵之間的關係。slots_to_keys跳躍表每個節點的分值(score)都是一個槽號,而每個節點的成員(number)都是一個數據庫鍵:

  • 每當節點往資料庫中新增一個新的鍵值對時,節點就會將這個鍵以及鍵的槽號關聯到slots_to_keys跳躍表;
  • 當節點刪除資料庫中的某個鍵值對時,節點就會在slots_to_keys跳躍表解除被刪除鍵與槽號的關聯;

通過在slots_to_keys跳躍表中記錄各個資料庫鍵所屬的槽,節點可以很方便地對屬於某個或某些槽的所有資料庫進行批量操作,例如命令Cluster GetKeySinSlot <slot> <count>命令可以返回最多count個屬於槽slot的資料庫鍵,而這個命令就是通過遍歷跳躍表實現的。

4、重新分片

Redis叢集的重新分片操作可以將任意數量已經分配給某個節點的槽改為指派給另一個節點,並且槽所屬的鍵值對也會移動到新節點。重新分片操作可以線上進行,在重新分片的過程中,叢集不需要下線,並且源節點和目標節點都可以繼續處理命令請求。

Redis叢集的重新分片操作是由Redis的叢集管理軟體redis_trib負責執行的,Redis提供進行重新分片所需的所有命令,而redis-trib則通過向源節點和目標節點發送命令來進行重新分片操作。redis-trib對叢集的單個槽slot進行重新分片的步驟如下:

  1. redis-trib對目標節點發送Cluster SetsLot <slot> Importing <source_id>命令,讓目標節點準備好從源節點匯入屬於槽的鍵值對;
  2. redis-trib對源節點發送Cluster SetsLot <slot> Migrating <target_id>命令,讓源節點準備好將屬於槽的鍵值對遷移到目標節點;
  3. redis-trib向源節點發送Cluster GetKeysSinslot <slot> <count>命令獲得最多count個屬於槽的鍵值對的鍵名;
  4. 對於上一步獲得的每個鍵名,redis-trib都向源節點發送一個Migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,將被選中的鍵原子地從源節點遷移至目標節點
  5. 重複執行第三第四步驟,直到源節點儲存的所有屬於槽Slot的鍵值對都被遷移至目標節點為至;
  6. redis-trib向叢集中的任意一個節點發送Cluster SetSlot <slot> Node <target_id>命令,將槽slot指派給目標節點,指派資訊會通過訊息傳送至整個叢集,最終叢集中的所有節點都會知道槽已經指派給了目標節點

5、ASK錯誤

在進行重新分片期間,源節點向目標節點遷移一個槽的過程中,可能會出現屬於被遷移槽的一部分鍵值對被儲存在源節點,而另一部分鍵值對則儲存在目標節點裡面。當客戶端向源節點發送一個與資料庫鍵有關的命令,並且命令要處理的資料又恰好屬於正在被遷移的槽時:

  • 源節點會先在自己的資料庫中查詢指定的鍵,如果找到的話,就直接執行客戶端傳送的命令;
  • 如果源節點沒有在自己的資料庫中找到指定的鍵,那麼這個鍵可能已經被轉移到目標節點,源節點將向客戶端返回一個ASK錯誤,指引客戶端轉向正在匯入槽的目標節點,並再次傳送需要執行的命令

1、Cluster SetSlot Importing命令的實現

clusterState結構的import_slots_from陣列記錄了當前節點正在從其他節點匯入的槽,如果import_slots_from[i]的值不為null,而是指向一個clusterNode結構,則表示當前節點正在從clusterNode所代表的節點匯入槽i.

在對叢集進行重新分片時,向目標節點發送Cluster SetLots <i> Importing <source_id>可以將目標節點clusterState,importing_slots_from[i]的值設定為source_id所代表節點的ClusterNode結構

2、Cluster SetSlot Migraing命令的實現

clusterState結構的migrating_slots_to陣列記錄了當前節點正在遷移至其他節點的槽,如果migrating_slots_to[i]的值不為null,而是指向一個clusterNode結構,則表示當前節點正在將槽i遷移至clusterNode所代表的節點

在對叢集進行重新分片時,向源節點發送命令Cluster SetLots <i> Migrating <target_id>可以將源節點clusterState.migrating_slots_to[i]的值設定為target_id所代表節點的clusterNode結構

3、ASK錯誤

如果節點收到一個鍵的命令請求,並且鍵所屬槽正好分配給了該節點,節點就會直接執行該命令,否則節點會檢查自己的ClusterState.migrating_slots_to[i],看鍵所屬的槽是否正在進行遷移,如果正在執行遷移,節點會像客戶端傳送ASK錯誤,引導客戶端到正在匯入槽的節點去查詢key(會在向目標節點轉發命令前傳送一條ASKING命令)

4、ASKING命令

ASKING命令唯一要做的就是打開發送該命令的客戶端的Redis_ASKING標識。一般情況下,如果客戶端向節點發送一個關於槽i的命令,而槽i有沒有指派給這個節點,那麼節點將返回MOVED錯誤,但是如果該節點的clusterState.importing_slots_from[i]顯示節點正在匯入槽i,並且傳送命令的客戶端帶有REDIS_ASKING標識,那麼節點將破例執行該命令一次

當客戶端接收到ASK錯誤並轉向正在執行匯入槽命令的節點時,客戶端會先向節點發送一個ASKING命令,然後再發送想要執行的命令,這時命令將會被源節點執行。需要注意的是,客戶端的Redis_Asking標識是一個一次性標識,當節點執行了一個帶有Redis_Asking標識的客戶端傳送的命令後,客戶端的Redis_Asking標識就會被移除

5、ASK錯誤和MOVED錯誤的區別

  • MOVES錯誤代表槽的負責權已經從一個節點轉移到了另一個節點,客戶端收到槽i的命令請求都會將請求傳送給MOVED錯誤所指向的節點;
  • ASK錯誤是兩個節點在遷移槽的過程中使用的一種臨時措施,客戶端在接收到槽i的ASK錯誤之後,客戶端只會在接下來的一次命令請求中將關於槽i的命令請求傳送至ASK錯誤所指示的節點,後續槽i的命令請求仍然會發送至目前負責處理槽i的節點

6、複製和故障轉移

Redis叢集中的節點分為主節點和從節點,其中主節點用於處理槽,而子節點用於複製某個主節點,並在被複制的主節點下線時,代替下線主節點繼續處理命令請求。

1、設定從節點

向一個節點發送命令Cluster RePlicate <node_id>可以讓接受命令的節點成為node_id所指定節點的從節點,並開始對主節點複製:

  1. 接收到該命令的節點首先會在自己的clusterState.nodes字典中找到node_id所對應節點的clusterNode結構,並將自己的clusterState.myself.slavedof指標指向這個結構,來記錄這個節點正在複製的主節點;
  2. 然後節點會修改自己在clusterState.myself.flags中的屬性,關閉原本的Redis_Node_Master標識,開啟Redis_Node_Slave標識,表示這個節點已經從主節點變為從節點;
  3. 最後,節點會呼叫複製程式碼,並根據clusterState.myself.slaveof指向的clusterNode結構所儲存的IP地址和埠號,對主節點進行復制。複製功能和單機Redis伺服器使用相同的功能,因而命令相同,相當於從節點發送命令Slaveof <master_ip> <master_port>

節點成為從節點並開始複製某個主節點的訊息會通過訊息傳送給叢集中的其他節點,最終叢集中的所有節點都會從知曉這一訊息。而叢集中的所有節點都會在代表主節點的clusterNode結構的slaves屬性和numslaves屬性中記錄正在複製這個主節點的從節點名單

2、故障檢測

叢集中的每個節點都會定期地向叢集中的其他節點發送PING訊息,來檢測對方是否線上,如果接受PING訊息的節點沒有在規定時間內向傳送PING訊息的節點返回PONG訊息,那麼傳送PING訊息的節點會將接受PING訊息的節點標記為疑似下線,即在clusterState.nodes字典中找到節點對應的clusterNode結構,並在結構的flags屬性中開啟Redis_NODE_PFail標識

叢集中的各個節點會通過互相傳送訊息的方式來交換叢集中各個節點的狀態資訊,如主節點A通過主節點B得知主節點C進入了疑似下線狀態,主節點A會在自己的clusterState.nodes字典中找到節點C對應的clusterNode結構,並將主節點B下線報告新增到clusterNode結構的fail_reports連結串列中,而每個下線報告由一個clusterNodeFailReport結構表示

如果在一個叢集裡面,半數以上覆制處理槽的主節點都將某個主節點x報告為疑似下線,那麼這個主節點x將被標記為已下線,將主節點標記為已下線的節點會向叢集廣播一條關於主節點x Fail的訊息,所有收到這條Fail訊息的節點都會立即將主節點x標記為已下線

3、故障轉移

當一個從節點發現自己正在複製的節點進入已下線狀態時,從節點將開始對下線主節點進行故障轉移,步驟如下:

  1. 複製下線主節點的所有從節點中,會有一個從節點被選中,並執行SLAVEOF no one命令,成為新的主節點;
  2. 新的主節點會撤銷所有對已知下線主節點的槽指派,並將這些槽全部指派給自己;
  3. 新的主節點會向叢集廣播一條PONG訊息,這條PONG訊息可以讓叢集中的其他節點知道這個節點已經由從節點變為了主節點,並且接管了原本由已下線節點負責處理的槽;
  4. 新的主節點開始接受和自己負責處理的槽有關的命令請求,故障轉移完成

4、選舉新的主節點

以下是新的主節點選舉方法的步驟,和領頭Sentinel的方法非常相似,因為都是基於Raft演算法的領頭選舉方法實現的:

  1. 叢集的配置紀元是一個自增計數器,初始值為0;
  2. 當叢集中的某個節點開始一次故障轉移操作時,叢集配置紀元的值會變為加一;
  3. 對於每個配置紀元,叢集中每個負責處理槽的主節點都只有1次投票機會,而第一個向主節點要求投票的從節點將獲得主節點的投票;
  4. 當從節點發現自己複製的主節點下線時,從節點會像叢集廣播以一條ClusterMsg_Type_FailOver_Auth_Request訊息,要求所有接收到這條訊息並且具有投票權的主節點向這個從節點投票;
  5. 如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼這個主節點將向要求投票的從節點返回一條ClusterMsg_Type_FailOver_Auth_Ack訊息,表示這個主節點支援從節點成為新的主節點;
  6. 每個參與選舉的從節點都會接受ClusterMsg_Type_FailOver_Auth_Ack訊息,並根據接收到的條數來統計自己獲得了多少主節點的支援;
  7. 如果叢集中有N個具有投票權的主節點,那麼當一個從節點收集到大於等於N/2+1張投票時,從幾點就會當選成為新的主節點;
  8. 因為在一個配置紀元中,每個具有投票權的主節點只能投一次票,所以獲得大於等於N/2+1張投票的從節點只會有一個;
  9. 如果在一個配置紀元中沒有從節點能後收集到足夠多的票,那麼叢集將進入一個新的紀元,並再次進行選舉直到新的主節點選出;

7、訊息

叢集中的各個節點通過傳送和收集訊息來進行通訊,傳送訊息的為傳送者,接受訊息的為接收者。節點發送的訊息主要有以下5種:

  • Meet訊息:當傳送者接收到客戶端傳送的Cluster Meet命令時,傳送者會向接收者傳送Meet訊息,請求接收者加入到傳送者的叢集中;
  • PING訊息:叢集中的每個節點預設每隔一秒就會從已知節點列表中隨機選出5個節點,然後對5個節點中最長時間沒有傳送PING訊息的節點發送PING訊息,來檢測是否線上。另外如果節點A最後一次收到節點B傳送的PONG訊息超過了cluster-node-timeout屬性的一半,節點A也會向節點B傳送PING訊息,防止對節點B的資訊更新滯後;
  • PONG訊息:當接收者收到傳送者發來的Meet訊息或ping訊息時,為了向傳送者確認這條Meet訊息或者PING訊息已經到達,接收者會向傳送者返回一條PONG訊息。另外,節點也可以通過向叢集廣播自己的PONG訊息讓叢集中的其他節點立即重新整理關於這個節點的認知(如從節點成為了新的主節點);
  • FAIL訊息:當一個主節點A判斷另一個主節點B已經進入FAIL狀態時,節點A會像叢集廣播一條關於節點B的FAIL訊息,所有接收到這條訊息的節點都會立即將節點B標記為已下線;
  • PUBLISH訊息:當節點接收到一個PUBLISH訊息時,節點會執行這個命令,並向叢集廣播一條PUBLISJ訊息,所有接受到這條訊息的節點都會立即執行相同的PUBLISH訊息;

一個訊息由訊息頭和訊息正文組成,接下來將介紹訊息頭和5類訊息正文

1、訊息頭

節點發送的所有訊息都由一個訊息頭包裹,訊息頭除了包含訊息正文之外,還記錄了訊息傳送者自身的一些資訊,這些資訊也會被接收者用到,因而嚴格來講,訊息頭本身也是訊息的一部分。每個訊息頭由一個cluster.h/clusterMsg結構表示,如下:

clusterMsg結構的currentEpoch、Sender、mySlots等屬性記錄了傳送者自身的節點資訊,接收者會根據這些資訊,在自己的clusterState.nodes字典中找到傳送者對應的clusterNode結構,並對結構進行更新。如通過接收者為傳送者記錄的槽指派資訊,以及傳送者在訊息頭的myslots屬性記錄的槽指派資訊,接收者就可以判斷槽指派資訊是否發生了變化。

而sclusterMsg.data屬性指向聯合cluster.h/clusterMsgData,這個聯合就是訊息的正文:

2、MEET、PING、PONG訊息的實現

Redis叢集中的各個節點通過Gossip協議來交換各自關於不同節點的狀態資訊,其中Gossip協議由MEET、PING、PONG三種訊息實現,這三種訊息的正文都由兩個cluster.h/clusterMsgDataGossip結構組成。由於這三種訊息都使用相同的訊息正文,所以節點通過訊息頭的type屬性來判讀訊息的類別。

每次傳送MEET、PING、PONG訊息時,傳送者都從自己已知節點列表中隨機選出兩個節點,並將這兩個節點的資訊分別儲存到兩個clusterMsgDataGossip結構中。該結構記錄了被選中節點的名字,傳送者與被選中節點最後一次傳送和接受PING訊息和PONG訊息的時間戳,被選中節點的IP地址和埠號,以及被選中節點的標識值:

當接收者收到MEET、PING、PONG訊息時,接收者會訪問訊息正文中的兩個clusterMsgDataGossIp結構,並根據自己是否認識該結構中記錄的被選中節點來判斷進行哪種操作:

  • 如果被選中節點不存在於接收者的已知節點列表,即代表接收者是第一次接觸到被選中節點,接收者將根據結構中記錄的IP地址和埠號資訊,與被選中節點進行握手;
  • 如果被選中節點存在於接收者的已知節點列表,即代表接收者之前與被選中節點接觸過,接收者將根據結構記錄的資訊對被選中節點所對應的clusterNode結構進行更新

3、FAIL訊息的實現

當叢集中的主節點A將主節點B標註為已下線(FAIL)時,主節點A將向叢集廣播一條關於主節點B的FAIL訊息,所有接受到這條FAIL訊息的節點都會將主節點B標記為已下線

在叢集節點數量較大的情況下,使用Gossip協議來傳播節點的已下線資訊會給節點的資訊帶來一定的延遲,因為Gossip協議訊息需要一段時間才能傳播至整個叢集,因而使用FAIL訊息來實現。FAIL訊息的正文由cluster.h/clusterMsgDataFail結構表示,這個結構只包含一個nodeName屬性,該屬性記錄了已下線節點的名字。因為叢集中所有節點的名字都是唯一的,因而FAIL訊息只需要儲存下線節點的名字,接收到訊息的節點就可以根據這個名字來判斷哪個節點下線了

4、PUBLISH訊息的實現

當客戶端向叢集中的某個節點發送命令:PUBLISH <channel> <message>時,接收到PUBLISH命令的節點不僅會向channel頻道傳送訊息message,還會像叢集廣播一條PUBLISH訊息,所有接收到這條PUBLISH訊息的節點都會像channel頻道傳送message訊息。換句話說,向叢集中的某個節點發送命令PUBLISH <channel> <message>,將導致叢集中的所有節點都向channel頻道傳送message訊息

PUBLISH訊息的正文由cluster.h/clusterMsgDataPublish結構表示:

  • bulk_data屬性是一個位元組陣列,儲存了客戶端通過PUBLISH命令傳送給節點的channel引數和message引數;
  • channel_len和message_len分別儲存了channel引數的長度和message引數的長度,bulk_data的0位元組~channel_len-1位元組儲存的是channel引數;而channel_len位元組~channel_len+message_len-1位元組儲存的則是message引數

注意:實際上直接向所有節點廣播PUBLISH命令是最簡單的執行相同PUBLISH命令的方法,這也是Redis在複製PUBLISH命令時所用到的方法,但這不符合Redis叢集通過傳送和接受訊息進行通訊的規則,因而沒有采用