[Redis]-----第三部分 多機(jī)數(shù)據(jù)庫的實現(xiàn)

第三部分 多機(jī)數(shù)據(jù)庫的實現(xiàn)


[toc]


1. 復(fù)制

? 在Redis中,用戶可以通過執(zhí)行SLAVEOF命令或者設(shè)置slaveof選項,讓一個服務(wù)器去復(fù)制另一個服務(wù)器,我們把被復(fù)制的服務(wù)器成為主服務(wù)器,對主服務(wù)器進(jìn)行復(fù)制的服務(wù)器成為從服務(wù)器.也就是我們常說的主從復(fù)制.

? 命令使用方式如下:

redis> SLAVEOF xxx.xxx.xxx.xxx 6379

? 進(jìn)行復(fù)制的主從服務(wù)器雙方將==保存相同的數(shù)據(jù)==,概念上稱為==服務(wù)器狀態(tài)一致==.

(1). 舊版復(fù)制功能的實現(xiàn)(Redis 2.8之前)

? Redis的復(fù)制功能分為同步(將從服務(wù)器的狀態(tài)更新至主服務(wù)器)和命令傳播(主服務(wù)器被更改后,讓主從服務(wù)器重新一致)兩個操作.

1). 同步

? 當(dāng)客戶端向發(fā)送了SLAVEOF命令,==進(jìn)入主從復(fù)制模式==的時候,從服務(wù)器首先要進(jìn)行同步操作,也就是==將從服務(wù)器中的數(shù)據(jù)更新為主服務(wù)器的數(shù)據(jù)庫狀態(tài)==.

? 通過SYNC命令完成,執(zhí)行步驟如下:

  1. 從服務(wù)器向主服務(wù)器發(fā)送SYNC命令
  2. 主服務(wù)器收到后執(zhí)行BGSAVE命令,生成一個RDB文件后使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令
  3. 主服務(wù)器的BGSAVE執(zhí)行完之后,主服務(wù)器將RDB文件發(fā)給從服務(wù)器,從服務(wù)器進(jìn)行加載,這時從服務(wù)器就會更新成主服務(wù)器執(zhí)行BGSAVE命令前的數(shù)據(jù)庫狀態(tài)
  4. 主服務(wù)器將命令緩沖中的命令全部發(fā)送給從服務(wù)器,從服務(wù)器執(zhí)行完之后達(dá)到主從一致的效果

2). 命令傳播

? 當(dāng)主服務(wù)器執(zhí)行寫命令的時候,主服務(wù)器的數(shù)據(jù)庫狀態(tài)就可能被改變,導(dǎo)致打破主從一致.

? 為了保持主從一致,主服務(wù)器需要將自己執(zhí)行的寫命令發(fā)送給從服務(wù)器執(zhí)行,從服務(wù)器執(zhí)行后,從新回到主從一致狀態(tài).

(2). 舊版功能的缺陷

? 對于從服務(wù)器從來沒有復(fù)制過任何服務(wù)器的初次復(fù)制情況,這種方案能夠完成任務(wù).但是如果是從服務(wù)器由于網(wǎng)絡(luò)原因斷線后的重新復(fù)制來說,效率就很低了.

? 因為這時還是會使用RDB文件作為同步的媒介,很大一部分?jǐn)?shù)據(jù)是重復(fù)的.

(3). 新版復(fù)制功能的實現(xiàn)

? Redis從2.8版本開始使用PSYNC命令代替SYNC命令來執(zhí)行復(fù)制時的同步操作.

? PSYNC具有完成重同步和部分重同步兩種模式:

  1. 完整重同步用于初次復(fù)制,和SYNC命令基本一樣
  2. 部分重同步用于斷線后的重復(fù)制,如果條件允許,主服務(wù)器可以將主從服務(wù)器連接斷開期間執(zhí)行的寫命令發(fā)送給從服務(wù)器

(4). 部分重同步的實現(xiàn)

? 部分重同步功能由一下三部分構(gòu)成:

  1. 主服務(wù)器的膚質(zhì)偏移量和從服務(wù)器的復(fù)制偏移量
  2. 從服務(wù)器的復(fù)制積壓緩存
  3. 服務(wù)器的運行ID

1). 復(fù)制偏移量

? 主服務(wù)器和從服務(wù)器都會維護(hù)一個復(fù)制偏移量.

  • 主服務(wù)器每次向從服務(wù)器傳播N個字節(jié)數(shù)據(jù)時,九江自己的復(fù)制偏移量加上N
  • 從服務(wù)器每次收到數(shù)據(jù)也會更新復(fù)制偏移量

? 通過==對比==主從服務(wù)器的==復(fù)制偏移量==就可以知道主從服務(wù)器是否處于主從一致狀態(tài)

2). 復(fù)制積壓緩沖區(qū)

? 復(fù)制積壓緩沖區(qū)是主服務(wù)器維護(hù)的一個==固定長度先進(jìn)先出隊列==,默認(rèn)大小1MB.內(nèi)部存儲了一系列命令的字節(jié)數(shù)組表示和其中每一個字節(jié)對應(yīng)的復(fù)制偏移量(也就是說,是可以清楚的知道每一條命令的復(fù)制偏移量).

? 主服務(wù)器進(jìn)行==命令傳播==時,不僅僅會將命令==發(fā)給從服務(wù)器==,還==寫入復(fù)制積壓緩沖區(qū)中==.因此復(fù)制積壓緩沖區(qū)中會保存最新的1MB命令數(shù)據(jù).當(dāng)從服務(wù)器重新連上主服務(wù)器時,從服務(wù)器根據(jù)自己的復(fù)制偏移量決定接下來的操作:

  1. 如果從服務(wù)器的復(fù)制偏移量不在復(fù)制積壓緩沖區(qū)中,那么進(jìn)行完整重同步操作
  2. 否則,說明從服務(wù)器中缺失的命令在復(fù)制積壓緩沖區(qū)中都存在,根據(jù)復(fù)制偏移量選擇起始位置進(jìn)行部分重同步操作

3). 服務(wù)器運行ID

? 每一個Redis服務(wù)器,不論主從都有自己的運行ID,這個ID由服務(wù)器啟動時自動生成,是一個40位十六進(jìn)制數(shù).在進(jìn)行主從復(fù)制時,從服務(wù)器會保存主服務(wù)器的運行ID.

? 在進(jìn)行斷線重連之后,從服務(wù)器會將之前自己保存的主服務(wù)器的運行ID發(fā)送給新的主服務(wù)器,如果一致,那么說明這是一次重連,進(jìn)行部分重同步操作.不一致則說明兩次連接的不是一個服務(wù)器,要進(jìn)行完整重同步操作.

(5). PSYNV命令的實現(xiàn)

? PSYNC命令的調(diào)用方法有兩種:

  1. 如果從服務(wù)器從來沒有連接過主服務(wù)器,或者已經(jīng)主動斷開了,那么在開始第一次新的復(fù)制時,主動請求服務(wù)器進(jìn)行完整重同步.
  2. 如果從服務(wù)器已經(jīng)父之過某個服務(wù)器,那么將之前復(fù)制的主服務(wù)器的運行ID和自己的復(fù)制偏移量發(fā)送給主服務(wù)器,由主服務(wù)器決定進(jìn)行哪一種同步操作;

? 主服務(wù)器的回應(yīng)會是下面三種之一:

  1. 如果主服務(wù)器要與從服務(wù)器進(jìn)行完整重同步,那么會把主服務(wù)器的運行ID發(fā)送給從服務(wù)器保存,將主服務(wù)器的復(fù)制偏移量發(fā)送給從服務(wù)器作為復(fù)制偏移量的起始值
  2. 如果主服務(wù)器要與從服務(wù)器進(jìn)行部分重同步,那么從服務(wù)器只需要等到主服務(wù)器將自己缺少的數(shù)據(jù)發(fā)送過來即可
  3. 如果主服務(wù)器的版本低于2.8,識別不了PSYNC命令,那么會讓從服務(wù)器重新發(fā)送一個SYNC命令進(jìn)行完整的重同步

(6). 復(fù)制的實現(xiàn)

? 通過向服務(wù)器(這里是從服務(wù)器)發(fā)送SLAVEOF命令也可以讓服務(wù)器去和其他服務(wù)器進(jìn)行主從連接.

1). 步驟1:設(shè)置主服務(wù)器的地址和端口

? 當(dāng)客戶端向服務(wù)器發(fā)送SLAVEOF命令請求時,從服務(wù)器首先要做的就是將發(fā)送來給定的主服務(wù)器IP和端口保存在服務(wù)器狀態(tài)中:

struct redisServer{
    // .....
    
    // 主服務(wù)器的地址
    char *masterhost;
    // 主服務(wù)器端口
    int masterport;
    
    // .....
}

? 然后服務(wù)器會向客戶端回復(fù)OK,隨后才開始真正執(zhí)行命令的內(nèi)容.

2). 步驟2:建立連接套接字

? 從服務(wù)器根據(jù)命令設(shè)置的IP和端口,創(chuàng)建連向主服務(wù)器的套接字連接,如果成功創(chuàng)建,那么從服務(wù)器將為這個套接字關(guān)聯(lián)一個專門處理復(fù)制工作的事件處理器,復(fù)制工作的所有內(nèi)容在后面都是由這個套接字完成的,

? 主服務(wù)器在接受這個套接字連接后,會創(chuàng)建響應(yīng)的客戶端狀態(tài),并將從服務(wù)器當(dāng)做鏈接到主服務(wù)器的一個客戶端來看待.

3). 步驟3:發(fā)送PING命令

? 從服務(wù)器成為主服務(wù)器的客戶端后,第一件事就是發(fā)送PING命令,PING命令有兩個作用:

  1. 檢查套接字的讀寫狀態(tài)是否正常
  2. 檢查主服務(wù)器能否正常處理命令請求

? 從服務(wù)器會收到三種響應(yīng)之一:

  1. 主服務(wù)器發(fā)送了一個命令回復(fù),但是從服務(wù)器并沒有在有限時間內(nèi)讀取出回復(fù)的內(nèi)容,那么說明當(dāng)前的網(wǎng)絡(luò)連接狀態(tài)不佳.會進(jìn)行斷開重連
  2. 主服務(wù)器向從服務(wù)器返回一個錯誤,表示主服務(wù)器暫時沒辦法處理從服務(wù)器的命令請求.從服務(wù)器會進(jìn)行斷開重連
  3. 從服務(wù)器收到PONG回復(fù),那么說明狀況良好,可以進(jìn)行下面的操作

4). 步驟4:身份驗證

? 從服務(wù)器收到PONG回復(fù)后,如果設(shè)置了masterauth,那么進(jìn)行身份驗證.

? 從服務(wù)器將向主服務(wù)器發(fā)送一條AUTH命令,參數(shù)為從服務(wù)器masterauth選項的值.

? 從服務(wù)器可能遇到的情況:

  1. 主服務(wù)器沒有設(shè)置requirepass選項,并且從服務(wù)器沒有設(shè)置masterauth,那么跳過這個階段
  2. 如果從服務(wù)器通過AUTH命令發(fā)送的密碼和主服務(wù)器requirepass選項設(shè)置的相同,那么驗證成功,繼續(xù)進(jìn)行
  3. 如果主從服務(wù)器只有一個進(jìn)行了設(shè)置,那么返回一個錯誤

5). 步驟5:發(fā)送端口信息

? 從服務(wù)器向主服務(wù)器發(fā)送從服務(wù)器監(jiān)聽的端口號.主服務(wù)器收到后,會記錄在從服務(wù)器的客戶端狀態(tài)中.

6). 步驟6:同步

? 從服務(wù)器向主服務(wù)器發(fā)送PSYNC命令,執(zhí)行同步操作.

? 值得一提的是,在執(zhí)行同步操作過后,主服務(wù)器也將成為從服務(wù)器的客戶端(雙方可以相互發(fā)送請求和回復(fù)),因為無論怎樣,主服務(wù)器都需要向從服務(wù)器發(fā)送命令請求來同步數(shù)據(jù).

7). 步驟7:命令傳播

? 主從服務(wù)器進(jìn)入命令傳播階段,主服務(wù)器一直想自己執(zhí)行的寫命令發(fā)送給從服務(wù)器,從服務(wù)器一直接收并執(zhí)行就可以保持主從一致.

(7). 心跳檢測

? 從服務(wù)器會以每秒一次的頻率向服務(wù)器發(fā)送命令:

// 參數(shù)是從服務(wù)器的復(fù)制偏移量
REPLCONF ACK <replication_offset>

? 有三個作用:

  1. 檢測主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
  2. 輔助實現(xiàn)min-slaves選項
  3. 檢測命令丟失

1). 檢測網(wǎng)絡(luò)連接狀態(tài)

? 如果至服務(wù)器超過一秒沒有接收到REPLCONF ACK命令,那么主服務(wù)器就知道網(wǎng)絡(luò)連接出現(xiàn)問題了.

? 這個計時的值被稱為lag,應(yīng)該在0~1浮動.

2). 輔助實現(xiàn)min-slaves配置選項

? 這個選項可以防止主服務(wù)器在不安全的情況下執(zhí)行寫命令.

? 可以配置從服務(wù)器少于多少,lag值大于多少的情況下,主服務(wù)器不執(zhí)行寫命令.

3). 檢測命令丟失

? 主服務(wù)器接收到REPLCONF ACK命令,取出其中的從服務(wù)器復(fù)制偏移量,與自己的復(fù)制偏移量進(jìn)行對比,如果不一致,那么從復(fù)制積壓緩沖區(qū)中找到服務(wù)器缺少的數(shù)據(jù),重新發(fā)送給從服務(wù)器.

? 這部分和部分重同步操作的原理很像.

? Redis 2.8版本之前沒有檢測丟失的功能.

2. Sentinel

? Sentinel(哨兵)是Redis的高可用性解決方案:==有一個過著多個Sentinel實例組成的Sentinel系統(tǒng)可以監(jiān)視任意多個主服務(wù)器以及每一臺主服務(wù)器下的所有從服務(wù)器,當(dāng)被監(jiān)視的一臺主服務(wù)器下線時,自動將這臺主服務(wù)器下的某個從服務(wù)器升級為新的主服務(wù)器,然后繼續(xù)維持主從一致==.

[圖片上傳失敗...(image-305527-1590395250247)]

穩(wěn)定狀態(tài)1:

[圖片上傳失敗...(image-d22c78-1590395250247)]

主服務(wù)器下線:

[圖片上傳失敗...(image-3e5155-1590395250247)]

哨兵進(jìn)行調(diào)整:

[圖片上傳失敗...(image-2f92d5-1590395250247)]

  1. Sentinel會挑選原主服務(wù)器下的其中一個從服務(wù)器,將這個從服務(wù)器升級為主服務(wù)器
  2. Sentinel系統(tǒng)向原主服務(wù)器下的其他所有從服務(wù)器發(fā)送新的復(fù)制指令,建立新的主從復(fù)制
  3. Sentinel繼續(xù)監(jiān)視那個下線的服務(wù)器,當(dāng)這個服務(wù)器重新上線時,成為一個從服務(wù)器

(1). 啟動并初始化Sentinel

? 啟動Sentinel可以使用命令:

redis-sentinel /path/to/your/sentinel.conf
或者
redis-server /path/to/your/sentinel.conf --sentinel

? 一個Sentinel啟動時,會執(zhí)行以下步驟:

  1. 初始化服務(wù)器
  2. 將普通的Redis服務(wù)器使用的代碼轉(zhuǎn)換成Sentinel專用代碼
  3. 初始化Sentinel狀態(tài)
  4. 根據(jù)指定的配置文件,初始化Sentinel監(jiān)視的主服務(wù)器列表
  5. 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接

1). 初始化服務(wù)器

? Sentinel本質(zhì)上只是一個運行在特殊模式下的Redis服務(wù)器,但是因為執(zhí)行的工作不相同,所以初始化方式也不完全相同.比如:不進(jìn)行RDB和AOF數(shù)據(jù)恢復(fù)等等.

2). 使用Sentinel專用代碼

? Sentinel使用一些新的代碼代替原先的部分Redis代碼中,比如默認(rèn)端口號.使用不同的服務(wù)器命令表(因為執(zhí)行的命令完全不同)

? 這也解釋了Sentinel不具備有些Redis服務(wù)器的功能.

3). 初始化Sentinel狀態(tài)

? 服務(wù)器會初始化一個sentinelState結(jié)構(gòu)體,也就是Sentinel狀態(tài),保存了服務(wù)器中所有和Sentinel功能相關(guān)的狀態(tài).

struct sentinelState{
    // 當(dāng)前紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移
    uint64_t current_epoch;
    
    // 保存了所有被這個Sentinel監(jiān)視的主服務(wù)器
    // 鍵是主服務(wù)器的名字,值是sentinelRedisInstance結(jié)構(gòu)體指針
    dict *masters;
    
    //是否進(jìn)入TILT模式
    int tilt;
    
    //目前正在執(zhí)行的腳本的數(shù)量
    int running_scripts;
    
    // 進(jìn)入TILT模式的時間
    mstime_t tile_start_time;
    
    // 最后一次執(zhí)行時間處理器的時間
    mstime_t previous_time;
    
    // 一個FIFO隊列,包含了所有需要執(zhí)行的用戶腳本
    list *scripts_queue;
};

4). 初始化Sentinel狀態(tài)的masters屬性

? 每一個主服務(wù)器都對應(yīng)一個sentinelRedisInstance結(jié)構(gòu)體:

typedef struct sentinelRedisInstance{
    // 標(biāo)識值,記錄當(dāng)前實例的狀態(tài)
    int flags;
    
    // 實例的名字
    // 主服務(wù)器的名字從配置文件中得到
    // 從服務(wù)器和Sentinel的名字由Sentinel自動設(shè)置
    // 格式為ip:port
    char *name;
    
    // 實例的運行ID
    char *runid;
    
    // 配置紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移
    uint64_t config_epoch;
    
    // 實例的地址
    sentinelAddr *addr;
    
    //主觀下線的判定時間
    mstime_t down_after_period;
    
    // 客觀下線的投票數(shù)量
    int quorum;
    
    //進(jìn)行故障轉(zhuǎn)移時,可以同時對新的主服務(wù)器進(jìn)行同步的從服務(wù)器的數(shù)量
    int parallel_syncs;
    
    // 刷新故障遷移狀態(tài)的最大時限
    msmtime_t failover_timeout;
    
    // .....
}sentinelRedisInstance;

typedef struct sentinelAddr{
    char *ip;
    int port;
}sentinelAddr;

? Sentinel狀態(tài)的masters字典的初始化根據(jù)載入的配置文件進(jìn)行.也就是說,配置文件中應(yīng)該記錄所有要監(jiān)視的主服務(wù)器的屬性.

5). 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接

? 對于每個被監(jiān)視的主服務(wù)器來說,Sentinel會創(chuàng)建兩個異步網(wǎng)絡(luò)連接:

  1. 命令連接:用于專門向主服務(wù)器發(fā)送命令,并接收回復(fù)
  2. 訂閱連接:專門用于訂閱主服務(wù)器的__sentinel__:hello頻道

為什么需要訂閱連接:

? Redis目前的發(fā)布訂閱功能,發(fā)送的消息不會保存在Redis服務(wù)器中,一旦客戶端不在線就會丟失,所以只用一個專門的訂閱連接來接受該頻道的消息.

(2). 獲取主服務(wù)器信息

? Sentinel默認(rèn)會以每十秒的頻率通過命令連接來向被監(jiān)視的主服務(wù)器發(fā)送INFO命令,通過分析這個命令的回復(fù)來獲得對應(yīng)主服務(wù)器的狀態(tài).

? 通過INFO命令的回復(fù),Sentinel可以得到以下信息:

  1. 關(guān)于主服務(wù)器本身的信息:服務(wù)器運行ID,服務(wù)器角色等
  2. 關(guān)于主服務(wù)器屬下所有從服務(wù)器的信息,每個從服務(wù)器由一個"slave"字符串開頭的行記錄,每一行記錄了從服務(wù)器的ip和端口

? 根據(jù)主服務(wù)器本身的信息,Sentinel會對主服務(wù)器的實例結(jié)構(gòu)進(jìn)行更新.而從服務(wù)器的信息將被用于更新主服務(wù)器實例結(jié)構(gòu)的slaves屬性,這個屬性記錄了下屬的所有從服務(wù)器字典(鍵是從服務(wù)器名字ip:port,值是從服務(wù)器對應(yīng)的結(jié)構(gòu)),如果從服務(wù)器不存在,新建

(3). 獲取從服務(wù)器信息

? 當(dāng)Sentinel發(fā)現(xiàn)主服務(wù)器有新的從服務(wù)器出現(xiàn)時,Sentinel除了會建立對應(yīng)的實例結(jié)構(gòu)之外,還會創(chuàng)建鏈接到從服務(wù)器的命令連接和訂閱連接.也就是說,從服務(wù)器和Sentinel也是直接相連的.

? 創(chuàng)建命令連接之后,Sentinel也會每隔十秒型從服務(wù)器發(fā)送依次INFO命令,獲得從服務(wù)器的運行id,角色,所連接的主服務(wù)器的id和端口,連接狀態(tài),從服務(wù)器的優(yōu)先級,復(fù)制偏移量.根據(jù)這些信息,對從服務(wù)器的實例結(jié)構(gòu)進(jìn)行更新.

(4). 向主服務(wù)器和從服務(wù)器發(fā)送信息

? 默認(rèn)情況下,Sentinel會以每兩秒一次的頻率向所有被連接的主從服務(wù)器發(fā)送一條命令,這條命令會向服務(wù)器的__sentinel__:hello頻道發(fā)送一條信息,包含一下內(nèi)容:

參數(shù) 意義
s_ip Sentinel的ip
s_port 端口
s_runid 運行id
s_epoch 配置紀(jì)元
m_name 主服務(wù)器的名字
m_ip ip
m_port 端口
m_rpoch 當(dāng)前配置紀(jì)元

? 如果是從服務(wù)器,那么其中的主服務(wù)器屬性指的是當(dāng)前進(jìn)行復(fù)制的主服務(wù)器的屬性.

(5). 接受來自主服務(wù)器和從服務(wù)器的頻道信息

? 當(dāng)Sentinel和一個服務(wù)器建立訂閱連接以后,Sentinel就會通過訂閱連接向服務(wù)器發(fā)送以下命令:

SUBSCRIBE __sentinel__:hello

? Sentinel對__sentinel__:hello頻道的訂閱會一致持續(xù)到Sentinel與服務(wù)器的連接斷開.

? 也就是說,Sentinel不僅僅可以向__sentinel__:hello頻道發(fā)送消息,也可以從__sentinel__:hello頻道讀取消息.

? 對于監(jiān)視同一個服務(wù)器的多個Sentinel來說,一個Sentinel發(fā)送的消息會被其他Sentinel接收到,這些消息會被用于更新其他Sentinel對于這個服務(wù)器的認(rèn)知.

1). 更新sentinels字典

? Sentinel為主服務(wù)創(chuàng)建的實例結(jié)構(gòu)中的sentinels字典會保存除了自己之外其他連接這個主服務(wù)器的Sentinel的資料.這個字典的鍵是其中一個Sentinel的名字(ip:port),值是對應(yīng)的Sentinel的實例結(jié)構(gòu)

? 當(dāng)Sentinel(目標(biāo)Sentinel)接收到其他Sentinel(源Sentinel)發(fā)來的消息時,就會從中得到以下信息:

  1. 源Sentinel的ip,端口,運行ID和配置紀(jì)元
  2. 源Sentinel正在監(jiān)視的主服務(wù)器的名字,ip,端口和配置紀(jì)元

? 根據(jù)這些消息,Sentinel會主服務(wù)器的sentinels字典

2). 創(chuàng)建連向其他Sentinel的命令連接

? 當(dāng)Sentinel通過頻道信息發(fā)現(xiàn)一個新的Sentinel是,不僅會更新sentinels字典,還會創(chuàng)建連向這個Sentinel的命令連接.

? ==最終監(jiān)視同一主服務(wù)器的多個Sentinel將形成相互連接的網(wǎng)絡(luò).==

(6). 檢測主觀下線狀態(tài)

? 默認(rèn)情況下,Sentinel會以每秒一次的頻率向所有與它創(chuàng)建了命令連接的實例(主服務(wù)器,從服務(wù)器,其他Sentinel),發(fā)送PING命令,名通過實例返回的PING命令回復(fù)判斷是否在線.

? 返回的回復(fù)中處理+PONG,-LOADING,-MASTERDOWN之外都是無效回復(fù).當(dāng)連續(xù)返回?zé)o效回復(fù)時長達(dá)到設(shè)定的主觀下線時間長度之后,Sentinel就會修改這個實例的實際結(jié)構(gòu)為主觀下線.

? 不同的Sentinel可能設(shè)置的主觀下線時長不同,當(dāng)一個Sentinel認(rèn)為某個實例主觀下線之后,其他的Sentinel可能認(rèn)為這個實例是在線的.

(7). 檢查客觀下線狀態(tài)

? 當(dāng)Sentinel認(rèn)為某個主服務(wù)器主觀下線后,為了進(jìn)行確認(rèn),它會向同樣監(jiān)視這個主服務(wù)器的其他Sentinel進(jìn)行詢問,當(dāng)Sentinel從其他Sentinel收集到足夠數(shù)量的答復(fù)(主觀下線或者客觀下線)能夠做出判斷之后,Sentinel就會判定主服務(wù)器為客觀下線,并進(jìn)行故障轉(zhuǎn)移.

1). 發(fā)送SENTINEL is-master-down-by-addr命令

? 該命令的參數(shù):詢問是否下線的主服務(wù)器的ip,duank,該Sentinel的配置紀(jì)元,Sentinel的運行id(這個也可以是*代替)

2). 接收SENTINEL is-master-down-by-addr命令

? 接收這個命令的Sentinel會提取出其中的各個參數(shù),返回向源Sentinel返回一個回復(fù).回復(fù)包含:該主服務(wù)器是否下線,局部領(lǐng)頭Sentinel的運行id和配置紀(jì)元.

3). 接收SENTINEL is-master-down-by-addr,命令的回復(fù)

? 源Sentinel接收到所有監(jiān)視這個主服務(wù)器的Sentinel的回復(fù)后,就得到了認(rèn)為該服務(wù)器下線的Sentinel的數(shù)量,通過和配置指定的判斷客觀下線的數(shù)量進(jìn)行對比(超過,則下線),就能得出是否客觀下線的結(jié)論.并對這個主服務(wù)器對應(yīng)結(jié)構(gòu)的標(biāo)記屬性進(jìn)行修改.

? 注意,客觀下線的標(biāo)準(zhǔn)在各個Sentinel中也是不一樣的,所以也會出現(xiàn)不同的Sentinel認(rèn)知不一樣的情況.

(8). 選舉領(lǐng)頭Sentinel

? 當(dāng)一個主服務(wù)器被認(rèn)為客觀下線時,監(jiān)視這個主服務(wù)器的各個Sentinel會進(jìn)行協(xié)商,選舉出一個領(lǐng)頭Sentinel,由這和Sentinel對這個下線的主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作.選舉的規(guī)則和方法:

  1. 每個在線的Sentinel都有資格
  2. 每次選舉,不論是否成功,配置紀(jì)元都自增1
  3. 在一個配置紀(jì)元中(選舉之后,自增,一直到下一次自增中間的時間),只有一個Sentinel是領(lǐng)頭Sentinel,不可被更改
  4. 每個發(fā)現(xiàn)主服務(wù)器客觀下線的Sentinel都會要求其他Sentinel將自己設(shè)置為局部領(lǐng)頭Sentinel
  5. SENTINEL is-master-down-by-addr命令的參數(shù)中運行id不為*,則表示自己要做局部領(lǐng)頭Sentinel
  6. 局部Sentinel規(guī)則先到先得
  7. SENTINEL is-master-down-by-addr命令的回復(fù)中有源Sentinel的局部領(lǐng)頭Sentinel的運行id和配置紀(jì)元,如果配置紀(jì)元相同,且運行id是自己,那么就說明發(fā)送回復(fù)的Sentinel將自己設(shè)置成了局部領(lǐng)頭
  8. 如果一個Sentinel被半數(shù)以上的Sentinel設(shè)置為局部領(lǐng)頭Sentinel,那么它成為領(lǐng)頭Sentinel
  9. 如果在給定的時間內(nèi)沒有完成選舉,那么會重新進(jìn)行

進(jìn)行選舉的SENTINEL is-master-down-by-addr命令和進(jìn)行客觀下線判斷的SENTINEL is-master-down-by-addr不重疊.

(9). 故障轉(zhuǎn)移

? 選舉出領(lǐng)頭Sentinel后,由這個Sentinel對已經(jīng)下線的主服務(wù)器進(jìn)行故障轉(zhuǎn)移.包含以下三個步驟:

  1. 從這個主服務(wù)器的下屬中選出一個,升級為主服務(wù)器
  2. 讓其他下屬從服務(wù)器對這個新的主服務(wù)器進(jìn)行主從連接
  3. 將已經(jīng)下線的主服務(wù)器設(shè)置為新主服務(wù)器的從服務(wù)器,當(dāng)它重新上線時,直接得到身份

1). 選出新的主服務(wù)器

? 領(lǐng)頭Sentinel會將下線的主服務(wù)器的從服務(wù)器保存到一個列表中,然后按照以下規(guī)則進(jìn)行過濾:

  1. 刪除下線或者斷線的從服務(wù)器
  2. 刪除5秒內(nèi)沒有回復(fù)過領(lǐng)頭Sentinel的INFO命令的從服務(wù)器
  3. 刪除與主服務(wù)器斷開連接超過設(shè)定值*10毫秒的從服務(wù)器(保證從服務(wù)器中的數(shù)據(jù)較新)

? 然后,按照從服務(wù)器的優(yōu)先級進(jìn)行選擇,選出其中優(yōu)先級最高的從服務(wù)器(相同優(yōu)先級選擇復(fù)制偏移量大的,然后進(jìn)一步選擇運行id小的),充當(dāng)新的主服務(wù)器.

2). 修改從服務(wù)器的復(fù)制目標(biāo)

? 領(lǐng)頭Sentinel向其他所有從服務(wù)器發(fā)送SLAVEOF命令來讓其他的從服務(wù)器都與新的主服務(wù)器建立主從連接.

3). 將舊的主服務(wù)器變?yōu)閺姆?wù)器

? 領(lǐng)頭Sentinel還是發(fā)送SLAVEOF命令完成這一步.

3. 集群

? Redis集群是Redis提供的分布式數(shù)據(jù)庫方案,集群通過分片來進(jìn)行數(shù)據(jù)共享,并提供復(fù)制和故障轉(zhuǎn)移功能.

(1). 節(jié)點

? 一個Redis有多個節(jié)點構(gòu)成.剛開始,每一個Redis服務(wù)器本身都可以被視為一個單個節(jié)點的集群,它們相互聯(lián)通后就構(gòu)成了一個多個節(jié)點的集群.

? 連接各個節(jié)點可以使用以下命令完成:

CLUSTER MEET <ip> <port>

? 向一個服務(wù)器發(fā)送這個命令,會讓接收命令的服務(wù)器向ip:port的服務(wù)器進(jìn)行握手,握手成功時就會將ip:port對應(yīng)的節(jié)點添加到接收命令的服務(wù)器所處的集群中.

1). 啟動節(jié)點

? 一個節(jié)點就是一個運行在集群模式下的Redis服務(wù)器,Redis服務(wù)器會在啟動時根據(jù)ckuster-enabled配置選項是竇唯yes來決定是否開啟服務(wù)器的集群模式.

? 單機(jī)Redis的所有功能,組件和數(shù)據(jù)都沒有發(fā)生改變.Redis會將集群模式下使用到的數(shù)據(jù)保存到cluster.h/clusterNode結(jié)構(gòu),cluster.h/clusterLink結(jié)構(gòu)和cluster.h/clusterState結(jié)構(gòu)中.

2). 集群數(shù)據(jù)結(jié)構(gòu)

? clusterNode結(jié)構(gòu)保存了一個節(jié)點當(dāng)前的狀態(tài),每一個節(jié)點都會創(chuàng)建一個clusterNode結(jié)構(gòu)保存自己的狀態(tài),并為集群中的其他節(jié)點各自創(chuàng)建一個該結(jié)構(gòu)體.

// 節(jié)點信息
struct clusterNode{
    // 創(chuàng)建節(jié)點的時間
    mstime_t ctime;
    
    // 節(jié)點的名字,字符數(shù)組的長度是固定的
    char name[REDIS_CLUSTER_NAMELEN];
    
    // 節(jié)點表示
    // 記錄節(jié)點的角色(主從)和所處狀態(tài)(上線,下線)
    int falgs;
    
    // 節(jié)點當(dāng)前的配置紀(jì)元
    uint64_t configEpoch;
    
    // 節(jié)點的ip
    char ip[REDIS_IP_STR_LEN];
    
    // 端口號
    int port;
    
    // 保存連接節(jié)點所需的有關(guān)信息
    clusterLink *link;
    
    // .....
};

// 與這個節(jié)點連接的相關(guān)信息
typedef struct clusterLink{
    // 連接的創(chuàng)建時間
    mstime_t ctime;
    // TCP套接字描述符
    int fd;
    // 輸出緩沖區(qū)
    sds sndbuf;
    // 輸入緩沖區(qū)
    sds rcvbuf;
    // 與這個節(jié)點所相連的所有節(jié)點
    struct clusterNode *node;
}clusterLink;

// 當(dāng)前節(jié)點視角下的集群狀態(tài)
// 每一個節(jié)點中都存儲集群信息
typedef struct clusterState{
    // 指向當(dāng)前節(jié)點的指針
    clusterNode *myself;
    
    // 結(jié)群當(dāng)前的配置紀(jì)元
    uint64_t currentEpoch;
    
    // 集群當(dāng)前的狀態(tài)(在線或則下線)
    int state;
    
    // 集群中至少處理著一個槽的節(jié)點的數(shù)量
    int size;
    
    // 集群節(jié)點的名單(包括myself節(jié)點)
    dict *nodes;
    
    // .....
}clusterState;

3). CLUSTER MEET命令的實現(xiàn)

? 客戶端向節(jié)點A發(fā)送CLUSTER MEET,讓A去和節(jié)點B握手:

  1. A會為B創(chuàng)建一個clusterNode結(jié)構(gòu),并添加到自己的clusterState.nodes字典中
  2. 節(jié)點A根據(jù)命令中的ip和端口向B發(fā)送一條MEET消息
  3. B接收到消息之后為A創(chuàng)建clusterNode結(jié)構(gòu),添加到自己的clusterState.nodes字典中
  4. B向A返回一條PONG消息,通過這個返回,A可以知道B準(zhǔn)備好了
  5. A向B返回一條PING消息,通過這個消息B知道A成功收到了自己的反饋
  6. 握手完成
  7. A節(jié)點會將B節(jié)點的信息通過Gossip協(xié)議傳播給集群中的其他節(jié)點,讓其他節(jié)點與B進(jìn)行握手
  8. 一段時間后,B會被集群中的所有節(jié)點認(rèn)識

(2). 槽指派

? Redis集群通過分片的方式保存數(shù)據(jù)庫中的鍵值對:集群的整個數(shù)據(jù)庫被分為16384個槽,數(shù)據(jù)庫中的每一個鍵被分配給這16384個槽中的一個,每個節(jié)點可以處理0~16384個槽.當(dāng)數(shù)據(jù)庫中的所有槽都被處理時,集群處于上線狀態(tài),否則就是下線狀態(tài).

? 以下命令可以將一個或者多個槽指派給接==受命令的節(jié)點==負(fù)責(zé):

CLUSTER ADDSLOTS <slot> [slot ...]

// 下面這個命令可以將0~5000的槽都交給127.9.9.1:7000的節(jié)點處理
127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 ... 5000

1). 記錄節(jié)點的槽指派信息

? clusterNode結(jié)構(gòu)的slots屬性和numslot屬性記錄了節(jié)點負(fù)責(zé)處理那些事情:

struct clusterNode{
    // .....
    // 二進(jìn)制為數(shù)組,包含16384個二進(jìn)制位,每一位表示這個槽是否被這個節(jié)點處理
    unsigned char slots[16384/8];
    
    // 這個節(jié)點處理的槽的數(shù)量
    int nummslots;
    
    // .....
};

2). 傳播節(jié)點的槽指派信息

? 一個節(jié)點除了負(fù)責(zé)記錄自己的slots屬性和numslots屬性之外,還會通過消息將自己的這兩個屬性發(fā)送給集群中的其他所有節(jié)點.當(dāng)其他節(jié)點收到時,就會保存在自己節(jié)點的clusterState結(jié)構(gòu)中的slots數(shù)組中.

? 所以集群中的每一個節(jié)點都能知道任意一個槽被分派的節(jié)點.

3). 記錄集群所有槽的指派信息

? clusterState結(jié)構(gòu)中的slots數(shù)組記錄了所有槽的指派信息:

typedef struct clusterState{
    // .....
    
    clusterNode *slots[16384];
    
    // .....
}clusterState;

? slots數(shù)組中包含16384個clusterNode指針,分別代表了每一個槽的分派節(jié)點.

這個數(shù)組存在的意義是:O(1)時間復(fù)雜度獲取某個槽的分派節(jié)點

但是如果只有這個數(shù)組存儲分派信息,那么在分派信息的傳播時,就會一次又一次的遍歷這個數(shù)組,得到每一個節(jié)點的負(fù)責(zé)區(qū)域.很低效.

4). CLUSTER ADDSLOTS命令的實現(xiàn)

? CLUSTER ADDSLOTS命令接受一個或者多個槽作為參數(shù),將這個槽指派給接收命令的節(jié)點負(fù)責(zé)

? 偽代碼實現(xiàn):

def CLUSTER_ADDSLOTS(*all_input_slots):
    # 檢查傳入的槽是否有已經(jīng)被分派的
    for i in all_input_slots:
        if clusterState.slots[i] != NULL:
            # 如果有,報錯
            reply_error()
            return
    
    # 再次遍歷
    for i in all_input_slots:
        # 更新這個槽的負(fù)責(zé)節(jié)點指針
        clusterState.slots[i] = clusterState.myself
        # 更新負(fù)責(zé)節(jié)點中slots屬性對應(yīng)位上的值為1
        setSlotBit(clusterState.myself.slots, i)
    
    # 告訴其他所有節(jié)點,自己目前正在負(fù)責(zé)的槽
    call_all_other_node()

(3). 在集群中執(zhí)行命令

? 對數(shù)據(jù)庫中的16384個槽都進(jìn)行了指派之后,集群就會進(jìn)入上線狀態(tài),這是客戶端就可以向集群中的節(jié)點發(fā)送數(shù)據(jù)命令了.

? 當(dāng)客戶端向一個節(jié)點發(fā)送一個命令請求時,接收命令的節(jié)點會計算出命令要處理的數(shù)據(jù)庫鍵處于的槽,并判斷這個槽是不是自己負(fù)責(zé)的,如果是,那么自己處理這個命令.如果不是,那么向客戶端返回一個MOVED錯誤,引導(dǎo)客戶端轉(zhuǎn)向正確的節(jié)點,再次發(fā)送要執(zhí)行的命令.

1). 計算鍵屬于哪個槽

? 算法同以下偽代碼:

def slot_number(key):
    # CRC16()方法用于計算key的CRC-16校驗和
    # 然后進(jìn)行除法散列
    return CRC16(key) & 16383

2). 判斷槽是否由當(dāng)前節(jié)點負(fù)責(zé)

? 從clusterState.slots中取出這個槽對應(yīng)的節(jié)點指針,和clusterState.myself進(jìn)行對比即可.

3). MOVED錯誤

? MOVED錯誤的格式為:

MOVED <slot> <ip>:<port>

? 內(nèi)容包括當(dāng)前key的槽,和負(fù)責(zé)這個槽的節(jié)點的ip:端口

? 一個連接集群的客戶端通常會與集群中的多個節(jié)點創(chuàng)建連接,這里客戶端就會根據(jù)MOVED返回的信息選擇正確的套接字發(fā)送命令.如果碰巧這時這個節(jié)點未與當(dāng)前客戶端連接,那么會先根據(jù)ip和端口創(chuàng)建套接字連接,然后在進(jìn)行轉(zhuǎn)向.

集群模式的客戶端由以下命令啟動:

redis-cil -c -p <port>

集群模式下MOVED錯誤是被隱藏的,客戶端會自行進(jìn)行節(jié)點的轉(zhuǎn)向,用戶是察覺不到的.單機(jī)模式下就會打印出MOVED錯誤.

4). 節(jié)點數(shù)據(jù)庫的實現(xiàn)

? 節(jié)點只能使用0號數(shù)據(jù)庫.

? 其他保存鍵值對和設(shè)置過期時間的方式和單機(jī)數(shù)據(jù)庫完全一樣.

? 但是節(jié)點數(shù)據(jù)庫會用clusterState結(jié)構(gòu)中的slots_to_keys跳躍表來==存儲槽和鍵==的關(guān)系,其中分值表示槽號,成員是數(shù)據(jù)庫鍵.

? 這樣就可以批量的對某個節(jié)點中的一個槽進(jìn)行統(tǒng)一處理.

typedef struct clusterState{
    // .....
    zskiplist *slots_to_keys;
    // .....
}clusterState;

(4). 重新分片

? Redis的重新分片操作可以將任意的已經(jīng)進(jìn)行了指派的槽重新指派給另一個節(jié)點,這個操作可以在線進(jìn)行.

? Redis集群的重新分派有Redis的集群管理軟件redis-trib負(fù)責(zé)執(zhí)行.Redis提供了命令,redis-trib通過向源節(jié)點和目標(biāo)節(jié)點發(fā)送命令完成.過程如下:

  1. redis-trib對目標(biāo)節(jié)點發(fā)送命令讓其做好準(zhǔn)備接受節(jié)點(CLUSTER SETSLOT <slot> IMPORTING <source_id>)
  2. redis-trib對源節(jié)點發(fā)送命令讓其做好遷移槽的準(zhǔn)備(CLUSTER SETSLOT <slot> MIGRATING <target_id>)
  3. redis-trib對源節(jié)點發(fā)送命令獲得==不小于一定數(shù)量的某個槽中的key==(CLUSTER GETKEYSINSLOT <slot> <count>)
  4. redis-trib向源節(jié)點發(fā)送命令,將3過程中被選中的鍵值對原子性的遷移到目標(biāo)節(jié)點
  5. 重復(fù)執(zhí)行3和4,直到全部遷移完成
  6. 向集群中任意一個節(jié)點發(fā)送命令,告知已經(jīng)將某個槽重新指派給了新的節(jié)點,其他的節(jié)點更新自己內(nèi)部的信息

(5). ASK錯誤

? 在重新分片時,有可能會遇到一個槽中的鍵值對只遷移了一部分的情況,這個時候,如果對這些鍵進(jìn)行訪問,集群會先在源節(jié)點中查找,如果沒有找到,則表示有可能會出現(xiàn)在目標(biāo)節(jié)點中.(也可能這個鍵不存在)

? 這時源節(jié)點會向客戶端返回一個ASK錯誤,引導(dǎo)客戶端轉(zhuǎn)向目標(biāo)節(jié)點再次發(fā)送命令.

1). CLUSTER SETSLOT IMPORTING命令的實現(xiàn)

? clusterState結(jié)構(gòu)中的importing_slots_from數(shù)組記錄了當(dāng)前節(jié)點正在從其他節(jié)點導(dǎo)入的槽

typedef struct clusterState{
    // .....
    clusterNode *importing_slots_from[16384];
    
    // .....
}clusterState;

? 該數(shù)組中的元素是clusterNode指針,指向源節(jié)點

? 重新分片中這個命令就是更改目標(biāo)節(jié)點中的這個數(shù)組,從而在后面發(fā)生ASK錯誤的時候,可以分辨.

2). CLUSTER SETSLOT MIGRATING命令的實現(xiàn)

? clusterState結(jié)構(gòu)中的migrating_slots_to數(shù)組記錄了當(dāng)前節(jié)點正在遷移至其他節(jié)點的槽.

typedef struct clusterState{
    // .....
    clusterNode *migrating_slots_from[16384];
    
    // .....
}clusterState;

? 這個命令更新了源節(jié)點中的這個屬性.

3). ASK錯誤

? 如果一個節(jié)點接收到一個鍵key的請求,并且這個鍵所屬的槽i正好被指派給當(dāng)前節(jié)點,那么就會嘗試在自己數(shù)據(jù)庫中查找這個key.如果找到了,就執(zhí)行客戶端發(fā)送的命令.

? 如果沒找到,那么會先檢查自己的migrating_slots_from數(shù)組中是否記錄這個槽正在被遷移.如果在遷移,那么向客戶端發(fā)送一個ASK錯誤,引導(dǎo)其去目標(biāo)節(jié)點中尋找.如果沒在被遷移,那么表示這個key不存在.

ASK <slot_id> <host>:<port>

? 接到ASK錯誤的客戶端根據(jù)努錯誤提供的ip和端口轉(zhuǎn)向目標(biāo)節(jié)點,然后先向目標(biāo)節(jié)點發(fā)送一個ASKING命令,然后重新發(fā)送原先的命令請求.

4). ASKING命令

? ASKING命令的唯一目的就是打開發(fā)送該命令客戶端的REDIS_ASKING標(biāo)識.

? 一般情況下,未完成重新分片的情況下,目標(biāo)節(jié)點是不知道自己負(fù)責(zé)這個槽的.但是當(dāng)自己的importing_slots_from數(shù)組顯示這個槽正在被導(dǎo)入自己,并且發(fā)送來命令的客戶端帶有REDIS_ASKING標(biāo)識.那么這個節(jié)點就會破例執(zhí)行關(guān)于這個沒有完成導(dǎo)入的槽的命令.

? 客戶端的REDIS_ASKING標(biāo)識使用過一次就會被清除.

5). ASK錯誤的MOVED錯誤的區(qū)別

? MOVED錯誤指這個槽的負(fù)責(zé)權(quán)已經(jīng)交給另一個節(jié)點,而ASK錯誤是指當(dāng)前節(jié)點雖然還保存這個槽的負(fù)責(zé)權(quán),但是這個槽正在被轉(zhuǎn)移,并且當(dāng)前要查找的key恰好未找到,懷疑被轉(zhuǎn)移了.

? MOVED錯誤會改變客戶端對槽的認(rèn)知,也就是說,會通知客戶端下次尋找這個槽時,直接去正確的節(jié)點.而ASK錯誤沒有這種功能,只是臨時的進(jìn)行一次處理.

(6). 復(fù)制與故障轉(zhuǎn)移

? Redis集群中的節(jié)點是分主從的,主節(jié)點負(fù)責(zé)處理槽,從節(jié)點用于復(fù)制某個主節(jié)點,并在復(fù)制的主節(jié)點下線時進(jìn)行升級代替.

1). 設(shè)置從節(jié)點

? 向一個節(jié)點發(fā)送命令:

CLUSTER REPLICATE <node_id>

? 可以讓接受命令的節(jié)點成為node_id所指定節(jié)點的從節(jié)點,并開始進(jìn)行復(fù)制.

  1. 接受帶這個命令的節(jié)點現(xiàn)在自己的clusterState.nodes字典中找到對應(yīng)的主節(jié)點的clusterNode,并將自己的clusterState.myself.slaveof指針指向這個結(jié)構(gòu),表示正在復(fù)制這個主節(jié)點
  2. 修改自己在clusterState.myself.flags中的屬性,將標(biāo)識由主節(jié)點修改為從節(jié)點
  3. 調(diào)用復(fù)制代碼,根據(jù)主節(jié)點的clusterNode結(jié)構(gòu)中的ip和端口進(jìn)行復(fù)制
  4. 然后這個節(jié)點就成為了主節(jié)點的從節(jié)點
  5. 這個信息會通過消息發(fā)送給集群中的其他節(jié)點,所有的節(jié)點都會在各自的內(nèi)存中的該主節(jié)點的clusterNode中進(jìn)行記錄

2). 故障檢測

? 集群中每個節(jié)點都會定期地向集群中的其他節(jié)點發(fā)送PING命令,當(dāng)其他節(jié)點一定時間內(nèi)沒有返回PONG命令之后,就會認(rèn)為這個節(jié)點疑似下線,并在clusterState的nodes字典中找到疑似下線的節(jié)點的clusterNode結(jié)構(gòu),將其的標(biāo)識位標(biāo)識為疑似下線.

? 集群中的每一個節(jié)點都會通過互相發(fā)送消息來傳遞這些信息.

? 當(dāng)A節(jié)點從別處得知節(jié)點B疑似下線時,會在自己的clusterState的nodes字典中找到節(jié)點B對應(yīng)的clusterNode結(jié)構(gòu),在其內(nèi)部添加一條下線報告(這時A節(jié)點還并不主觀認(rèn)為B節(jié)點疑似下線).

struct clusterNode{
    // .....
    
    // 一個鏈表,記錄了其他節(jié)點對該節(jié)點的下線報告
    list *fail_reports;
    
    // .....
};

// 下線報告的結(jié)構(gòu)體
struct clusterNodeFailReport{
    // 報告這個節(jié)點下線的節(jié)點
    struct clusterNode *node;
    
    // 最后一次從node節(jié)點收到下線報告的時間
    // 防止報告過期
    mstime_t time;
};

? 如果在一個集群中,半數(shù)以上的主節(jié)點都將某個主節(jié)點x標(biāo)記為主觀疑似下線,那么這個主節(jié)點x將被標(biāo)記為==已下線==,并且進(jìn)行標(biāo)記的節(jié)點會向集群發(fā)送廣播告知整個集群這個消息.收到這個廣播的節(jié)點更改下線節(jié)點的狀態(tài)和標(biāo)識.

3). 故障轉(zhuǎn)移

? 當(dāng)一個從節(jié)點發(fā)現(xiàn)自己正復(fù)制的主節(jié)點已進(jìn)入下線狀態(tài)時,從節(jié)點將對下線主節(jié)點進(jìn)行故障轉(zhuǎn)移:

  1. 選舉新的主節(jié)點
  2. 被選中的從節(jié)點執(zhí)行SLAVEOF no one命令,成為新的主機(jī)誒單
  3. 新的主節(jié)點撤銷對已下線主節(jié)點的槽指派,并將這些槽指派給自己
  4. 新的主節(jié)點向集群廣播一條PONG消息,告知所有節(jié)點自己替代了原來的主節(jié)點,并且接管了原先的槽
  5. 新的主節(jié)點開始接收和處理有關(guān)于槽的命令請求.

4). 選舉新的主節(jié)點

  1. 集群的配置是一個自增計數(shù)器,初始為0
  2. 每次進(jìn)行依次故障轉(zhuǎn)移操作時,集群的配置紀(jì)元會自增1
  3. 對于每個配置紀(jì)元,每個主節(jié)點都有一次投票的機(jī)會.而第一個向主節(jié)點要求投票的從節(jié)點將獲得主節(jié)點的投票
  4. 當(dāng)從節(jié)點發(fā)現(xiàn)自己的主節(jié)點下線時,會發(fā)廣播,通知所有主節(jié)點對自己投票
  5. 如果一個主節(jié)點還沒進(jìn)行過投票時,收到了廣播,那么進(jìn)行相應(yīng)節(jié)點的投票
  6. 參與選舉的從節(jié)點通過之前廣播的回應(yīng)進(jìn)行統(tǒng)計,得到自己的票
  7. 當(dāng)一個節(jié)點你的票數(shù)過主節(jié)點數(shù)量的一半時,成為主節(jié)點
  8. 如果再一次選舉中沒有一個從節(jié)點獲得半數(shù)投票,那么進(jìn)入新的紀(jì)元,從新選舉

(7). 消息

? 集群中各個節(jié)點通過發(fā)送和接受消息來進(jìn)行通信.節(jié)點發(fā)送的消息主要有一下五種:

  1. MEET消息:申請加入集群的消息
  2. PING消息:集群例每個節(jié)點每隔一秒就會在自己已知的所有節(jié)點中隨機(jī)選出5個,然后給其中最久沒有發(fā)送過PING消息的節(jié)點發(fā)送PING消息,以此來檢測是否該節(jié)點在線.另外,如果當(dāng)前結(jié)點最后一次收到某個節(jié)點的PONG消息的時長超過了設(shè)置值的一半,就會主動向這個節(jié)點發(fā)送PING消息
  3. PONG消息:接收到MEET消息或者PING消息時,PONG消息可以認(rèn)為是回應(yīng).另外,一個節(jié)點可以通過向集群廣播自己的PONG消息來讓其他節(jié)點刷新對自己的認(rèn)知(主從節(jié)點身份更換)
  4. FAIL消息:A節(jié)點向B節(jié)點發(fā)送FAIL消息表示自己認(rèn)為C節(jié)點已經(jīng)下線,得到這個消息的節(jié)點都要更新對下線節(jié)點的認(rèn)知
  5. PUBLISH消息:當(dāng)一個節(jié)點收到PUBLISH命令是,節(jié)點會執(zhí)行這個命令,并向集群廣播一條PUBLISH消息,所有接收到這個消息的節(jié)點都會執(zhí)行同樣的命令

? 每個消息都由消息頭組成,消息頭又封裝了正文和發(fā)送者自身的一些信息.

1). 消息頭

typedef struct {
    // 消息的長度(整個消息頭,包括正文等等)
    uint32_t totlen;
    
    // 消息的類型
    uint16_t type;
    
    // 正文包含的節(jié)點信息數(shù)量
    uint16_t count;
    
    // 發(fā)送者所處的配置紀(jì)元
    uint64_t currentEpoch;
    
    // 如果發(fā)送者是一個主節(jié)點,那么這里是發(fā)送者的配置紀(jì)元
    // 如果是一個從節(jié)點,這里記錄的是所復(fù)制的主節(jié)點的配置紀(jì)元
    uint64_t configEpoch;
    
    // 發(fā)送者的名字
    char sender[REDIS_CLUSTER_NAMELEN];
    
    // 發(fā)送者目前的槽指派信息
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
    
    // 正在復(fù)制的主節(jié)點的名字,如果當(dāng)前節(jié)點是主節(jié)點,那么為某個常量
    char slaveof[REDIS_CLUSTER_NAMELEN];
    
    // 發(fā)送者的端口
    uint16_t port;
    
    // 發(fā)送這的標(biāo)識
    uint16_t flags;
    
    // 發(fā)送者所處的集群狀態(tài)
    unsigned char state;
    
    // 消息的正文
    union clusterMsgData data;
}clusterMsg;

// 正文的聯(lián)合屬性
union clusterMsgData{
    // MEET,PING,PONG消息的正文
    struct{
        // 每條MEET,PING,PONG消息都包含兩個clusterMsgDataGossip結(jié)構(gòu)
        clusterMsgDataGossip gossip[1];
    }ping;
    
    // FAIL消息的正文
    struct{
        clusterMasDataFil about;
    }fail;
    
    // PUBLISH消息的正文
    struct{
        clusterMsgDataPublish msg;
    }publish;
    
    // 其他消息的正文
}

? clusterMsg結(jié)構(gòu)的currentEpoch,sender,myslots等屬性記錄了發(fā)送者本身的節(jié)點信息,接受者根據(jù)這些信息,在自己的clusterState中的nodes字典中找到這個節(jié)點,對結(jié)構(gòu)進(jìn)行更新.

2). MEET.PING,PONG消息的實現(xiàn)

? Redis集群中的各個節(jié)點通過Gossip協(xié)議來交換各自冠以不同節(jié)點的狀態(tài)信息,其中Gossip協(xié)議有Meet,Ping,Pong三種消息實現(xiàn).

? MEET.PING,PONG三種消息使用相同的消息頭,所以通過type字段來判斷是這三種中的哪一種.

? 每次發(fā)送這三種消息時,發(fā)送這都從自己已知的列表中隨機(jī)選出兩個節(jié)點(不分主從),將這兩個節(jié)點的信息分別保存到兩個clusterMsgDataGossip結(jié)構(gòu)中,其中記錄了一系列信息

typedef struct{
    // 節(jié)點名字
    char nodename[REDIS_CLUSTER_NAMELEN];
    // 最后一次向該節(jié)點發(fā)送PING消息的時間戳
    uint32_t ping_sent;
    // 最后一次從該節(jié)點接受PONG消息的時間戳
    uint32_t pongReceived;
    // 節(jié)點的ip
    char ip[16];
    // 端口
    uint16_t port;
    // 標(biāo)示值
    uint16_t flags;
}clusterMsgDataGossip;

? 當(dāng)接到這三種命令時,接受者如果不認(rèn)識被選中的節(jié)點,那么說明與接受者進(jìn)行握手,如果認(rèn)識,對節(jié)點的信息進(jìn)行更新.

3). FAIL消息的實現(xiàn)

typedef struct{
    // 記錄下線節(jié)點的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
}clusterMasDataFil;

4). PUBLISH消息的實現(xiàn)

? PUBLISH消息用于廣播一條命令.

? 命令格式如下:

PUBLISH <channel> <message>

? 意為向channel頻道發(fā)送消息massage,并進(jìn)行廣播,讓其他收到廣播的節(jié)點也向channel頻道發(fā)送message消息.

typedef struct{
    uint32_t channel_len;
    uint32_t message_len;
    // 數(shù)據(jù)內(nèi)容,長度為8字節(jié)是為了對齊,實際長度不一定
    // 其中的前channel_len字節(jié)保存的是channel參數(shù)
    // 后面的message_len字節(jié)保存的是message參數(shù)
    unsigned char bulk_data[8];
}clusterMsgDataPublish;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谣旁,一起剝皮案震驚了整個濱河市读慎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妇垢,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肉康,死亡現(xiàn)場離奇詭異闯估,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吼和,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門涨薪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炫乓,你說我怎么就攤上這事尤辱。” “怎么了厢岂?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵光督,是天一觀的道長。 經(jīng)常有香客問我塔粒,道長结借,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任卒茬,我火速辦了婚禮船老,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘圃酵。我一直安慰自己柳畔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布郭赐。 她就那樣靜靜地躺著薪韩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捌锭。 梳的紋絲不亂的頭發(fā)上谴咸,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天橘茉,我揣著相機(jī)與錄音,去河邊找鬼。 笑死鲫凶,一個胖子當(dāng)著我的面吹牛源哩,可吹牛的內(nèi)容都是我干的知态。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼倒得,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了夭禽?” 一聲冷哼從身側(cè)響起屎暇,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驻粟,沒想到半個月后根悼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡蜀撑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年挤巡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酷麦。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡矿卑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沃饶,到底是詐尸還是另有隱情母廷,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布糊肤,位于F島的核電站琴昆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏馆揉。R本人自食惡果不足惜业舍,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望升酣。 院中可真熱鬧舷暮,春花似錦、人聲如沸噩茄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绩聘。三九已至沥割,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間君纫,已是汗流浹背驯遇。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留蓄髓,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓舒帮,卻偏偏與公主長得像会喝,于是被迫代替她去往敵國和親陡叠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345