Redis設(shè)計(jì)與實(shí)現(xiàn)3:多機(jī)數(shù)據(jù)庫(kù)的實(shí)現(xiàn)

復(fù)制

復(fù)制功能是讓一臺(tái)Redis服務(wù)器復(fù)制另一臺(tái)服務(wù)器,也就是Master-Slave模式莫秆,通常用于實(shí)現(xiàn)讀寫(xiě)分離。該功能有兩種實(shí)現(xiàn)悔详,分別對(duì)應(yīng)2.8版本之前的老版本镊屎,和2.8(包括)之后的新版本。

2.8版本之前的實(shí)現(xiàn)

復(fù)制功能分為同步和命令傳播兩個(gè)操作茄螃。

  • 同步:將從服務(wù)器的數(shù)據(jù)庫(kù)更新至主服務(wù)器當(dāng)前所處的數(shù)據(jù)庫(kù)狀態(tài)缝驳。
    同步的步驟如下:
  1. 從服務(wù)器發(fā)送SYNC命令。
  2. 主服務(wù)器收到SYNC命令归苍,執(zhí)行BGSAVE在后臺(tái)生成一個(gè)RDB文件用狱,并用緩沖區(qū)記錄從現(xiàn)在開(kāi)始執(zhí)行的所有寫(xiě)命令。
  3. 當(dāng)RDB文件準(zhǔn)備好時(shí)霜医,把該文件發(fā)送給從服務(wù)器齿拂。
  4. 從服務(wù)器阻塞載入RDB文件(這段時(shí)間從服務(wù)器不能處理任何請(qǐng)求)。
  5. 主服務(wù)器把緩沖區(qū)里的命令發(fā)送給從服務(wù)器
  • 命令傳播:主服務(wù)器把寫(xiě)命令傳播到從服務(wù)器肴敛,使得主從服務(wù)器的數(shù)據(jù)庫(kù)狀態(tài)一致署海。比如當(dāng)主服務(wù)器執(zhí)行DEL key時(shí),會(huì)異步的把該命令發(fā)送給從服務(wù)器医男,使兩者狀態(tài)最終一致砸狞。

缺點(diǎn):如果從服務(wù)器中途短線,重連后需要重新執(zhí)行一遍同步操作镀梭,效率較低(生產(chǎn)RDB文件需要耗費(fèi)大量I/O刀森、CPU資源)。

2.8及之后版本的實(shí)現(xiàn)

新版本使用PSYNC命令替代SYNC报账,該命令具有完整重同步和部分重同步兩種模式研底。其中完整重同步的步驟跟舊版本的SYNC的命令類(lèi)似不再贅述,下面主要講部分重同步功能透罢。

新版本的實(shí)現(xiàn)中榜晦,主從服務(wù)器分別維護(hù)一份復(fù)制偏移量,記錄當(dāng)前復(fù)制的進(jìn)度羽圃。當(dāng)主服務(wù)器向從服務(wù)器發(fā)送N個(gè)字節(jié)的數(shù)據(jù)時(shí)就把自己的偏移量加上N乾胶,當(dāng)從服務(wù)器接收到N個(gè)字節(jié)的數(shù)據(jù)時(shí)就把自己的偏移量也加上N,如果主從服務(wù)器數(shù)據(jù)處于一致,那么它們的偏移量也是一致的识窿。

復(fù)制偏移量

如果從服務(wù)器出現(xiàn)了斷開(kāi)的狀況斩郎,那么復(fù)制偏移量就會(huì)和主服務(wù)器不一致:

復(fù)制偏移量

為了解決從服務(wù)器意外斷開(kāi)連接后能夠快速恢復(fù)到跟主服務(wù)器一致的狀態(tài)(之所以說(shuō)快速是因?yàn)榕f版本的實(shí)現(xiàn)效率太低),Redis使用了復(fù)制積壓緩沖區(qū)來(lái)記錄最近執(zhí)行的寫(xiě)命令喻频,以便在從服務(wù)器恢復(fù)連接后能通過(guò)緩沖區(qū)把丟失的寫(xiě)命令找回并發(fā)送到從服務(wù)器缩宜。該緩沖區(qū)是一個(gè)固定長(zhǎng)度的先進(jìn)先出隊(duì)列,默認(rèn)大小是1MB半抱,當(dāng)緩沖區(qū)大小不夠時(shí)會(huì)將位于隊(duì)首的元素拋棄脓恕,隊(duì)列保存了一部分最近傳播的寫(xiě)命令,每個(gè)字節(jié)的偏移量都會(huì)記錄在內(nèi)窿侈,其構(gòu)造如圖所示:

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

當(dāng)從服務(wù)器斷線重連后會(huì)發(fā)送自己的復(fù)制偏移量給主服務(wù)器,如果偏移量+1存在主服務(wù)器緩沖區(qū)中秋茫,那么主服務(wù)器會(huì)把這部分?jǐn)?shù)據(jù)發(fā)送給從服務(wù)器史简;反之會(huì)執(zhí)行完整重同步。

這里存在一個(gè)問(wèn)題肛著,從服務(wù)器重連后如何知道是否是之前的那臺(tái)主服務(wù)器圆兵?
其實(shí)Redis在啟動(dòng)時(shí)會(huì)生成一個(gè)隨機(jī)的服務(wù)器運(yùn)行ID,當(dāng)從服務(wù)器連接到主服務(wù)器后便會(huì)記下這個(gè)ID枢贿,下次連接時(shí)如果發(fā)現(xiàn)ID不能對(duì)應(yīng)就會(huì)執(zhí)行完整重同步殉农。

在命令傳播階段,從服務(wù)器默認(rèn)每秒向服務(wù)器發(fā)送心跳局荚,并帶上自己的復(fù)制偏移量超凳,如果主服務(wù)器超過(guò)1秒沒(méi)有收到心跳,說(shuō)明網(wǎng)絡(luò)出現(xiàn)了問(wèn)題耀态。此外如果主服務(wù)器發(fā)送的寫(xiě)命令意外丟失轮傍,那么主服務(wù)器通過(guò)心跳返回的偏移量就可以知道主從服務(wù)器狀態(tài)不一致,然后通過(guò)復(fù)制擠壓緩沖區(qū)補(bǔ)發(fā)缺失的命令首装,使兩者再次一致创夜。

Redis2.8版本之前沒(méi)有這一機(jī)制,如果主服務(wù)器向從服務(wù)器發(fā)送的命令出現(xiàn)了丟失仙逻,兩者都不會(huì)注意到驰吓。

Sentinel(哨兵)

上文介紹的復(fù)制功能可以把Master的讀壓力分散到其它Slave上,但是當(dāng)Master發(fā)生故障后系奉,需要手動(dòng)把一臺(tái)Slave提升為Master檬贰,客戶(hù)端也很有可能需要修改連接地址如果你沒(méi)有服務(wù)發(fā)現(xiàn)這樣的基礎(chǔ)設(shè)施的話,因?yàn)槟悴恢朗悄囊慌_(tái)Slave被提升為Master喜最。
Redis提供了Sentinel來(lái)解決以上問(wèn)題偎蘸。它會(huì)監(jiān)控所有的Redis節(jié)點(diǎn),并提供故障轉(zhuǎn)移機(jī)智,是一種高可用解決方案迷雪。其大致結(jié)構(gòu)如圖所示:

Sentinel系統(tǒng)

初始化Sentinel

通過(guò)以下命令啟動(dòng)Sentinel:

redis-sentinel /path/to/your/sentinel.config
//等價(jià)于
//redis-server /path/to/your/sentinel.config --sentinel

Sentinel本質(zhì)是一個(gè)特殊的Redis服務(wù)器限书,因此初始化過(guò)程可以看作是初始化一臺(tái)普通的Redis服務(wù)器,但在某些方面會(huì)有所區(qū)別章咧,比如它不會(huì)使用數(shù)據(jù)庫(kù)因而不會(huì)載入RDB或AOF文件倦西。哨兵使用的命令表也很普通Redis服務(wù)器不同,它只支持7個(gè)命令:PING赁严、SENTINEL扰柠、INFOSUBSCRIBE疼约、UNSUBSCRIBE卤档、PSUBSCRIBEPUNSUBSCRIBE程剥。

啟動(dòng)后劝枣,服務(wù)器會(huì)初始化一個(gè)sentinelState結(jié)構(gòu)的對(duì)象,保存了所有和哨兵功能相關(guān)的狀態(tài)织鲸,其結(jié)構(gòu)如下所示:

struct sentinelState {

    //當(dāng)前紀(jì)元舔腾,是個(gè)計(jì)數(shù)器,故障轉(zhuǎn)移時(shí)會(huì)用到
    uint64_t current_epoch;

    //保存了當(dāng)前sentinel監(jiān)控的所有master
    //鍵是master的名字搂擦,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
    dict *masters;

    //是否進(jìn)入TILT模式
    int tilt;

    //目前正在執(zhí)行的腳本數(shù)量
    int running_scripts;

    //進(jìn)入TILT模式的時(shí)間
    mstime_t tilt_start_time;

    //最后一次執(zhí)行時(shí)間處理器的時(shí)間
    mstime_t previous_time;

    //FIFO隊(duì)列稳诚,包含所有需要執(zhí)行的用戶(hù)腳本
    list *scripts_queue;

}

其中sentinelRedisInstance結(jié)構(gòu)如下所示:

struct sentinelRedisInstance {

    //實(shí)例的類(lèi)型以及狀態(tài)
    int flags;

    //實(shí)例的名字,master的名字在配置文件中配置瀑踢,slave的名字由ip:port組成
    char *name;

    //運(yùn)行ID
    char * runid;

    //配置紀(jì)元扳还,是個(gè)計(jì)數(shù)器,故障轉(zhuǎn)移時(shí)會(huì)用到
    uint64_t config_epoch;

    //實(shí)例的地址
    sentinelAddr *addr;

    //無(wú)響應(yīng)多少毫秒后會(huì)被判斷為主觀下線
    mstime_t down_after_period;

    //判斷實(shí)例為客觀下線所需要的支持投票數(shù)
    int quorum;

    //在故障轉(zhuǎn)移時(shí)可以同時(shí)對(duì)新的主服務(wù)器進(jìn)行同步的從服務(wù)器數(shù)量
    int parallel_syncs;

    //刷新故障遷移狀態(tài)的超時(shí)時(shí)間
    mstime_t failover_timeout;
    
    //從服務(wù)器丘损,鍵是ip:port普办,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
    dict *slaves;

    //哨兵,鍵是ip:port徘钥,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
    dict *sentinels;

    //其它
    ...
}

上面的數(shù)據(jù)結(jié)構(gòu)可以和下面的配置文件對(duì)應(yīng)起來(lái):

//sentinel.conf

//監(jiān)控主服務(wù)器的名字為master1衔蹲,地址是127.0.0.1,端口是6379
//判斷實(shí)例為客觀下線所需要的支持投票數(shù)是2
sentinel monitor master1 127.0.0.1 6379 2

//master1若在30000毫秒內(nèi)無(wú)響應(yīng)則判斷為下線
sentinel down-after-milliseconds master1 30000

//master1發(fā)生故障后呈础,它的從服務(wù)器同時(shí)只有1臺(tái)能從新的master進(jìn)行同步
sentinel parallel-syncs master1 1

//故障遷移超時(shí)時(shí)間900000毫秒
sentinel failover-timeout master1 900000

初始化完成后內(nèi)存中會(huì)有如下所示的對(duì)象關(guān)系:

對(duì)象結(jié)構(gòu)

Sentinel的一些狀態(tài)會(huì)持久化到配置文件中(sentinel.conf)舆驶,因此無(wú)需擔(dān)心sentinel重啟。

連接Master

初始化后sentinel會(huì)和每一個(gè)被監(jiān)視的master創(chuàng)建兩個(gè)連接而钞,一個(gè)用于收發(fā)命令沙廉,一個(gè)用于訂閱master的__sentinel__:hello頻道。

連接Master

連接建立后臼节,sentinel會(huì)以10秒一次的頻率向master發(fā)送INFO命令撬陵,通過(guò)返回值可以得到master的運(yùn)行ID珊皿、角色(master/slave)以及所有slave(可以看作是服務(wù)發(fā)現(xiàn))。
Sentinel會(huì)把slave關(guān)聯(lián)到對(duì)應(yīng)的master對(duì)象上:

關(guān)聯(lián)master和slave

連接Slave

通過(guò)master發(fā)現(xiàn)了所有slave之后巨税,sentinel會(huì)建立和slave的連接蟋定,同樣是每個(gè)slave兩個(gè)連接。連接建立后同樣會(huì)以10秒一次的頻率向slave發(fā)送INFO命令草添,并且得到運(yùn)行ID驶兜、master地址、復(fù)制偏移量等信息远寸,記錄到sentinelRedisInstance結(jié)構(gòu)中抄淑。

連接Sentinel

上面提到Sentinel會(huì)和master和slave保持兩個(gè)連接,一個(gè)用于收發(fā)命令驰后,一個(gè)用于發(fā)布訂閱指定的頻道肆资。Sentinel以2秒一次的頻率通過(guò)第2個(gè)連接向所有監(jiān)控的服務(wù)器發(fā)送以下格式的命令:

PUBLISH __sentinel__:hello "<sentinel_ip>,<sentinel_port>,<sentinel_runid>,<sentinel_epoch>,<master_name>,<master_ip>,<master_port>,<master_epoch>"
  • 以sentinel開(kāi)頭的參數(shù)是sentinel本身的信息。
  • 以master開(kāi)頭的參數(shù)是主服務(wù)器的信息灶芝,如果sentinel發(fā)送命令的對(duì)象是主服務(wù)器迅耘,那么就是該主服務(wù)器本身的信息;如果發(fā)送命令的對(duì)象是從服務(wù)器监署,那么就是該從服務(wù)器正在復(fù)制的主服務(wù)器的信息。

每一個(gè)sentinel既是__sentinel__:hello頻道的發(fā)布者纽哥,同時(shí)也是訂閱者钠乏。這種設(shè)計(jì)的作用是,當(dāng)一個(gè)服務(wù)器被多個(gè)sentinel監(jiān)控時(shí)春塌,任意一個(gè)sentinel發(fā)送的消息都會(huì)被其它sentinel接收到晓避。當(dāng)其它sentinel接收到消息后就會(huì)發(fā)現(xiàn)新的sentinel,并且更新master對(duì)應(yīng)的sentinelRedisInstance結(jié)構(gòu)的sentinels屬性只壳,如下圖所示:

更新sentinels屬性

通過(guò)這種方式俏拱,每個(gè)sentinel都知道它監(jiān)控的某個(gè)master還在被哪些sentinel監(jiān)控。

Sentinel會(huì)和其它sentinel建立1個(gè)連接吼句,最終同一個(gè)master的所有sentinel互聯(lián)锅必。


Sentinel互聯(lián)

判斷下線

一個(gè)sentinel會(huì)以每秒1次的頻率向所有建立連接的服務(wù)器發(fā)送PING命令,包括主從服務(wù)器和其它sentinel惕艳。如果在一段時(shí)間內(nèi)(由配置的down-after-milliseconds指定)一直收到無(wú)效回復(fù)(有效回復(fù)有3種搞隐,+PONG-LOADING远搪、-MASTERDOWN劣纲,此外都是無(wú)效回復(fù),沒(méi)有回復(fù)也是無(wú)效回復(fù))那么sentinel就會(huì)認(rèn)為該實(shí)例已經(jīng)下線谁鳍,sentinel會(huì)修改該實(shí)例對(duì)應(yīng)的sentinelRedisInstance結(jié)構(gòu)的flags屬性癞季,將其標(biāo)為主觀下線(SDOWN劫瞳,Subjectively Down)。

同一個(gè)master被多個(gè)sentinel監(jiān)控時(shí)绷柒,因?yàn)槊總€(gè)sentinel的主觀下線時(shí)長(zhǎng)可能配置了不同的值志于,因此不同的sentinel對(duì)于同一個(gè)master的下線狀態(tài)可能有不同的判斷。

當(dāng)sentinel認(rèn)為一個(gè)master已經(jīng)下線后辉巡,它會(huì)發(fā)送以下格式的命令詢(xún)問(wèn)該服務(wù)器的其它sentinel是否也認(rèn)為該服務(wù)器已經(jīng)下線:

SENTINEL is-master-down-by-addr <master_ip> <master_port> <sentinel_epoch> *
//e.g. SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *

當(dāng)另一個(gè)sentinel接收并且檢查master是否下線后會(huì)回復(fù)一條包含三個(gè)參數(shù)的消息:

1) <down_state> //1表示下線恨憎,0表示未下線
2) *
3) 0

如果得到的確認(rèn)數(shù)量超過(guò)了配置的quorum的值,sentinel就會(huì)把master標(biāo)為客觀下線(ODOWN郊楣,Objectively Down)憔恳。

Sentinel僅會(huì)對(duì)master進(jìn)行故障轉(zhuǎn)移,如果是slave下線了净蚤,sentinel會(huì)把它標(biāo)為SDOWN钥组,并且不會(huì)詢(xún)問(wèn)其它的sentinel。

Leader選舉

當(dāng)一個(gè)master被標(biāo)為客觀下線時(shí)今瀑,監(jiān)視這個(gè)服務(wù)器的各個(gè)sentinel會(huì)協(xié)商選舉出一個(gè)leader程梦,由leader執(zhí)行故障轉(zhuǎn)移。

我們假設(shè)有3個(gè)sentinel組成哨兵系統(tǒng)橘荠,為了選出leader屿附,3個(gè)sentinel再次向其它兩個(gè)sentinel發(fā)送命令,區(qū)別是這次會(huì)帶上sentinel自己的運(yùn)行ID哥童,表示要求對(duì)方把自己設(shè)為leader:

SENTINEL is-master-down-by-addr <master_ip> <master_port> <sentinel_epoch> <sentinel_runid>

如果接收到命令的sentinel還沒(méi)有設(shè)置過(guò)leader的話就會(huì)把接收到的sentinel設(shè)置為leader挺份,并返回:

1) <down_state> //1表示下線,0表示未下線
2) <leader_runid>
3) <leader_epoch>

接收到回復(fù)的sentinel就可以知道有多少sentinel選舉自己當(dāng)leader贮懈,如果獲得了半數(shù)以上(大于等于sentinel數(shù)量/2+1)的投票匀泊,那么就算選舉成功。

選舉leader

在選舉過(guò)程中有幾個(gè)規(guī)則:

  1. 不論選舉是否成功朵你,所有sentinel的配置紀(jì)元都會(huì)自增一次各聘。
  2. 在一個(gè)紀(jì)元里只能設(shè)置一次leader,設(shè)置的優(yōu)先級(jí)是先到先得抡医。
  3. 如果在給定時(shí)間內(nèi)選舉失敗躲因,那么會(huì)在一段時(shí)間后重新選舉直到選出leader為止。

Leader選舉算法請(qǐng)參考Raft算法魂拦。

故障轉(zhuǎn)移

Leader會(huì)從下線主服務(wù)器的所有從服務(wù)器中選出一臺(tái)并轉(zhuǎn)換為主服務(wù)器毛仪,有以下幾個(gè)篩選條件:

  1. slave處于在線狀態(tài)。
  2. 最近5秒內(nèi)回復(fù)過(guò)leader發(fā)出的INFO命令(來(lái)保證leader和該slave最近成功進(jìn)行過(guò)通訊)芯勘。
  3. slave與已經(jīng)下線的master連接斷開(kāi)時(shí)間不超過(guò)down-after-milliseconds * 10毫秒(確保slave沒(méi)有過(guò)早和master斷開(kāi)連接箱靴,slave保存的數(shù)據(jù)是相對(duì)較新的)。

篩選完成后荷愕,如果沒(méi)有可用的slave那么就終止此次故障轉(zhuǎn)移衡怀,否則Leader會(huì)根據(jù)slave的優(yōu)先級(jí)進(jìn)行排序棍矛,選出優(yōu)先級(jí)最高的slave。

Slave的優(yōu)先級(jí)可以在配置文件中通過(guò)slave-priority屬性進(jìn)行修改抛杨,默認(rèn)是100够委,該值越低,優(yōu)先級(jí)越高怖现,如果設(shè)成0茁帽,那么永遠(yuǎn)不會(huì)被當(dāng)選master∏停可以通過(guò)INFO命令查看slave的優(yōu)先級(jí)潘拨。

如果有多個(gè)slave優(yōu)先級(jí)相同,那么選出復(fù)制偏移量最大的slave饶号;如果多個(gè)slave復(fù)制偏移量相同铁追,那么選出運(yùn)行ID最小的slave。

Slave被選中后茫船,Leader會(huì)向它發(fā)送SLAVEOF no one命令琅束,同時(shí)以1秒1次的頻率發(fā)送INFO命令,觀察slave的角色從slave變成master算谈。此時(shí)leader就知道該slave已經(jīng)提升為master涩禀。如果這一步超時(shí)了,就終止此次故障轉(zhuǎn)移然眼。

下一步埋泵,Leader向已下線master的其它slave發(fā)送SLAVEOF命令,讓它們復(fù)制新的master罪治。
最后一步,當(dāng)已下線的master重新上線后礁蔗,Leader會(huì)向它發(fā)送SLAVEOF命令讓它成為新master的從服務(wù)器觉义。

TILT模式

由于sentinel系統(tǒng)依賴(lài)機(jī)器時(shí)間,比如需要知道多長(zhǎng)時(shí)間沒(méi)有跟某個(gè)實(shí)例進(jìn)行過(guò)通訊浴井,因此一旦機(jī)器的時(shí)間功能發(fā)生錯(cuò)誤晒骇,Redis就會(huì)進(jìn)入TILT模式,直到正常運(yùn)行超過(guò)30秒磺浙。在該模式下它不會(huì)執(zhí)行任何操作洪囤,比如故障轉(zhuǎn)移,當(dāng)其它sentinel發(fā)來(lái)SENTINEL is-master-down-by-addr命令詢(xún)問(wèn)實(shí)例的在線狀態(tài)時(shí)它會(huì)返回負(fù)值撕氧,告訴對(duì)方它的下線判斷不再準(zhǔn)確瘤缩。

判定時(shí)間功能發(fā)生錯(cuò)誤的依據(jù)是:sentinel定時(shí)器每100毫秒執(zhí)行一次,如果兩次時(shí)間差值是負(fù)值(時(shí)間出現(xiàn)了倒退)或者過(guò)大(超過(guò)了2秒)伦泥,Redis就會(huì)進(jìn)入TILT模式剥啤。

客戶(hù)端處理流程

官方推薦的客戶(hù)端處理流程是:

  1. 當(dāng)客戶(hù)端嘗試連接到一個(gè)sentinel系統(tǒng)時(shí)锦溪,依次嘗試連接sentinel實(shí)例,并發(fā)送SENTINEL get-master-addr-by-name master-name命令獲得master信息府怯,如果連接sentinel失敗或sentinel返回的master信息為null刻诊,那么繼續(xù)連接下一個(gè)sentinel,直到成功獲取master信息牺丙。
  2. 向master發(fā)送ROLE命令確認(rèn)該實(shí)例是master则涯,否則重復(fù)步驟1。
  3. 客戶(hù)端向sentinel訂閱頻道冲簿,當(dāng)master被切換后可以收到新的master信息粟判。
  4. 如果有讀寫(xiě)分離的需求,那么可以通過(guò)SENTINEL slaves master-name命令獲取slave列表民假。

集群

相比上文提到的哨兵模式浮入,集群模式主要提供了數(shù)據(jù)分片的功能,因?yàn)橐慌_(tái)服務(wù)器總有物理容量的限制羊异。分片功能支持把數(shù)據(jù)分散的存儲(chǔ)在多臺(tái)實(shí)例上事秀,突破了單個(gè)節(jié)點(diǎn)的物理限制。

* 快速搭建集群

Linux上可以使用docker快速搭建一個(gè)有三個(gè)master節(jié)點(diǎn)的集群:

# 以下腳本須在linux中執(zhí)行
# 因?yàn)閐ocker的host模式問(wèn)題野舶,mac下需要安裝linux虛擬機(jī)下面的腳本才能正常執(zhí)行

# 創(chuàng)建3個(gè)redis-server
docker run --name redis1 --net=host -itd redis redis-server --port 6379 --cluster-enabled yes --cluster-node-timeout 60000
docker run --name redis2 --net=host -itd redis redis-server --port 6380 --cluster-enabled yes --cluster-node-timeout 60000
docker run --name redis3 --net=host -itd redis redis-server --port 6381 --cluster-enabled yes --cluster-node-timeout 60000

# 使用redis-trib工具啟動(dòng)集群
docker run --rm --net=host -it zvelo/redis-trib create  127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381

啟動(dòng)

Redis服務(wù)器在啟動(dòng)時(shí)會(huì)通過(guò)cluster-enabled參數(shù)決定是否開(kāi)啟集群模式易迹。集群模式跟普通單機(jī)模式用到的模塊大部分都一樣(如RDB模塊),除此之外用到了一些集群專(zhuān)有的功能平道,比如serverCron方法會(huì)調(diào)用clusterCron發(fā)送Gossip消息睹欲、檢查節(jié)點(diǎn)狀態(tài)。

除了單機(jī)模式用到的redisServer結(jié)構(gòu)體一屋,集群模式下還會(huì)用到clusterNode窘疮、clusterLinkclusterState來(lái)存儲(chǔ)集群的一些狀態(tài)冀墨。

每個(gè)實(shí)例都有一個(gè)clusterState類(lèi)型的對(duì)象來(lái)記錄當(dāng)前集群的狀態(tài):

typedef struct clusterState{

    // 指向當(dāng)前節(jié)點(diǎn)的指針
    clusterNode *myself;
    
    // 配置紀(jì)元
    uint64_t currentEpoch;
    
    // 集群的狀態(tài):上線還是下線
    int state;

    // 集群中至少處理一個(gè)槽的節(jié)點(diǎn)的數(shù)量
    int size;

    // 集群所有的節(jié)點(diǎn)(包含自己)闸衫,字典的鍵是節(jié)點(diǎn)名字,值是節(jié)點(diǎn)對(duì)應(yīng)的clusterNode對(duì)象
    dict *nodes;

    // 槽
    // clusterNode *slots[16384];

} clusterState;

每個(gè)clusterState都存儲(chǔ)了集群內(nèi)所有節(jié)點(diǎn)的信息诽嘉,如IP蔚出、角色等。下面是一個(gè)更直觀的結(jié)構(gòu)圖:

clusterState

握手

建立集群的第一步是握手虫腋。通過(guò)以下命令讓遠(yuǎn)程實(shí)例加入當(dāng)前實(shí)例所在的集群:

CLUSTER MEET <ip> <port>

# 當(dāng)前實(shí)例 127.0.0.1 6379
# CLUSTER MEET 127.0.0.1 6380
# CLUSTER MEET 127.0.0.1 6381
# 以上三個(gè)節(jié)點(diǎn)形成集群

當(dāng)實(shí)例A向?qū)嵗鼴發(fā)送MEET消息并握手成功后骄酗,A會(huì)把B的信息以Gossip協(xié)議傳播給集群中的其它節(jié)點(diǎn),最終所有節(jié)點(diǎn)都會(huì)知道B的存在悦冀。

集群模式下趋翻,整個(gè)數(shù)據(jù)庫(kù)被劃分為16384個(gè)槽,每個(gè)鍵占用其中的一個(gè)槽盒蟆,每個(gè)節(jié)點(diǎn)可以處理0個(gè)或多個(gè)槽嘿歌。只有當(dāng)所有的槽都有節(jié)點(diǎn)在處理時(shí)掸掏,集群才處于上線狀態(tài)。因此即使用CLUSTER MEET命令建立了集群宙帝,集群仍然處于下線狀態(tài)丧凤。

使用以下命令分配槽:

CLUSTER ADDSLOTS <slot>
# 分配0-5槽
# CLUSTER ADDSLOTS 0 1 2 3 4 5

以上命令可以配合shell腳本分配全部的16384個(gè)槽。推薦使用redis-trib工具自動(dòng)建立集群并分配槽步脓。

clusterNode結(jié)構(gòu)里使用一個(gè)unsigned char slots[2048]屬性記錄節(jié)點(diǎn)處理的槽愿待。

2048 = 16384 / 8 , C語(yǔ)言中的char占1個(gè)字節(jié)。

一個(gè)char類(lèi)型變量占1個(gè)字節(jié)(8位)靴患,位如果是1則表示節(jié)點(diǎn)處理該槽仍侥,0表示不處理。其結(jié)構(gòu)如下:

每個(gè)Redis節(jié)點(diǎn)除了記錄自己處理的槽外也會(huì)記錄其它節(jié)點(diǎn)處理的槽鸳君,這些狀態(tài)以slots數(shù)組存儲(chǔ)在clusterState結(jié)構(gòu)體中农渊,每個(gè)元素指向一個(gè)clusterNode結(jié)構(gòu)體。在CLUSTER ADDSLOTS命令執(zhí)行完畢后或颊,節(jié)點(diǎn)會(huì)把自己的slots數(shù)組發(fā)送給其它的節(jié)點(diǎn)告訴他們處理槽的狀態(tài)砸紊。

slots數(shù)組

集群模式下,每個(gè)鍵都?xì)w屬于一個(gè)槽囱挑,一個(gè)槽可以對(duì)應(yīng)多個(gè)鍵醉顽。Redis使用CRC16算法將鍵映射到一個(gè)槽,算法如下:

CRC16(key) & 16383

可以通過(guò)以下命令查看鍵所屬的槽:

CLUSTER KEYSLOT <key>

當(dāng)節(jié)點(diǎn)計(jì)算出鍵屬于哪個(gè)槽后平挑,它會(huì)檢查所屬槽是不是自己負(fù)責(zé)處理游添,如果是,那么就執(zhí)行客戶(hù)端發(fā)來(lái)的命令通熄;否則節(jié)點(diǎn)會(huì)查找處理該槽的節(jié)點(diǎn)并向客戶(hù)端返回MOVED錯(cuò)誤指引它轉(zhuǎn)向正確的節(jié)點(diǎn)唆涝。

MOVED錯(cuò)誤的格式為:

MOVED <slot> <ip>:<port>

客戶(hù)端收到MOVED錯(cuò)誤后會(huì)根據(jù)ip和端口信息轉(zhuǎn)向目標(biāo)節(jié)點(diǎn)并重新發(fā)送命令。

客戶(hù)端也區(qū)分集群模式和單機(jī)模式唇辨。單機(jī)模式會(huì)直接打印出MOVED錯(cuò)誤而不會(huì)重定向石抡。集群模式需要加上-c參數(shù)。

數(shù)據(jù)庫(kù)

集群模式和單機(jī)模式下的數(shù)據(jù)庫(kù)一個(gè)重要的區(qū)別是:集群模式只能使用0號(hào)數(shù)據(jù)庫(kù)助泽。

集群模式下的鍵值對(duì)以及過(guò)期時(shí)間的存儲(chǔ)和普通模式下的一樣。除此之外集群模式下的節(jié)點(diǎn)會(huì)用一個(gè)跳躍表存儲(chǔ)槽和鍵的關(guān)系嚎京,用于對(duì)某些槽的鍵進(jìn)行批量操作嗡贺。跳躍表里的分值就是槽號(hào),值就是鍵鞍帝。下面是一個(gè)例子:

槽和鍵

重新分片

當(dāng)新增或移除節(jié)點(diǎn)時(shí)诫睬,需要對(duì)集群進(jìn)行重新分片。我們使用redis-trib工具對(duì)集群進(jìn)行在線重新分片操作帕涌,其主要步驟是:

  1. 向目標(biāo)節(jié)點(diǎn)發(fā)送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令摄凡,讓其做好準(zhǔn)備续徽。
  2. 向源節(jié)點(diǎn)發(fā)送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,讓其做好遷移的準(zhǔn)備亲澡。
  3. 向源節(jié)點(diǎn)發(fā)送CLUSTER GETKEYSINSLOT <slot> <count>命令钦扭,獲得最多count個(gè)位于slot槽的鍵。
  4. 對(duì)于步驟3中返回的每一個(gè)鍵床绪,向源節(jié)點(diǎn)發(fā)送一個(gè)MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令將選中的鍵原子地遷移到目標(biāo)節(jié)點(diǎn)客情。
  5. 重復(fù)步驟3和4直到所有的鍵都已被遷移。
  6. 向集群中任意一個(gè)節(jié)點(diǎn)發(fā)送CLUSTER SETSLOT <slot> NODE <target_id>命令癞己,將slot指派給目標(biāo)節(jié)點(diǎn)膀斋,這一消息隨后會(huì)發(fā)送到所有的節(jié)點(diǎn)。
  7. 如果有多個(gè)slot需要遷移痹雅,那么繼續(xù)對(duì)剩下的slot進(jìn)行上面的操作仰担。

ASK

在遷移過(guò)程中,當(dāng)客戶(hù)端訪問(wèn)源節(jié)點(diǎn)數(shù)據(jù)庫(kù)的某個(gè)鍵時(shí)绩社,源節(jié)點(diǎn)會(huì)先在自己的數(shù)據(jù)庫(kù)中查找摔蓝,如果沒(méi)有找到則檢查該鍵所屬槽的遷移狀態(tài)。slot的遷移狀態(tài)在源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)上各有一個(gè)clusterNode指針數(shù)組存儲(chǔ)(importing_slots_frommigrating_slots_to)铃将,其結(jié)構(gòu)如下:

slot遷移狀態(tài)

如果發(fā)現(xiàn)該鍵所屬的槽正在遷移项鬼,那么它會(huì)向客戶(hù)端返回ASK <target_ip>:<target:port>指引向目標(biāo)節(jié)點(diǎn)【⒀郑客戶(hù)端收到ASK命令后會(huì)向目標(biāo)節(jié)點(diǎn)發(fā)送ASKING消息然后再重新執(zhí)行之前發(fā)給源節(jié)點(diǎn)的命令绘盟。

ASKING命令的用處是打開(kāi)客戶(hù)端的REDIS_ADKING標(biāo)志以讓目標(biāo)節(jié)點(diǎn)強(qiáng)制執(zhí)行命令。如果按正常流程悯仙,此時(shí)槽還沒(méi)有完成遷移龄毡,仍舊屬于源節(jié)點(diǎn),所以如果不打開(kāi)特殊標(biāo)志锡垄,目標(biāo)節(jié)點(diǎn)會(huì)返回MOVED錯(cuò)誤沦零。

當(dāng)命令執(zhí)行完畢后,REDIS_ASKING標(biāo)志會(huì)被移除货岭。這是一個(gè)一次性的標(biāo)志路操,如果下一次客戶(hù)端訪問(wèn)時(shí)遷移仍未完成,那么仍舊會(huì)出現(xiàn)上面ASK的流程千贯。

* 例子

# 新增1個(gè)redis節(jié)點(diǎn)
docker run --name redis4 --net=host -itd redis redis-server --port 6382 --cluster-enabled yes --cluster-node-timeout 60000

# 使用redis-trib把新的節(jié)點(diǎn)加入到集群中
docker run --rm --net=host -it zvelo/redis-trib add-node  127.0.0.1:6382 127.0.0.1:6369

# 重新分片屯仗,分配1000個(gè)slot到新的節(jié)點(diǎn)上
# --from可以指定節(jié)點(diǎn),為了均勻分配搔谴,可以使用all魁袜,從所有舊的節(jié)點(diǎn)上平均的取出一部分slot遷移到新的節(jié)點(diǎn)上
# --slots指定本次遷到新節(jié)點(diǎn)的slot數(shù)量
docker run --rm --net=host -it zvelo/redis-trib reshard --from all --to <new_node_id>  --slots 1000 --yes 127.0.0.1:6379

復(fù)制

集群模式下節(jié)點(diǎn)也分為主節(jié)點(diǎn)和從節(jié)點(diǎn),從節(jié)點(diǎn)復(fù)制主節(jié)點(diǎn)并在主節(jié)點(diǎn)下線時(shí)接替它繼續(xù)處理請(qǐng)求。
通過(guò)向節(jié)點(diǎn)發(fā)送CLUSTER REPLICATE <master_id>讓接收命令的節(jié)點(diǎn)成為主節(jié)點(diǎn)的從節(jié)點(diǎn)峰弹。從節(jié)點(diǎn)會(huì)把clusterState.myself.slaveof指向主節(jié)點(diǎn)對(duì)應(yīng)的clusterNode結(jié)構(gòu)體店量,關(guān)閉REDIS_NODE_MASTER標(biāo)識(shí)并打開(kāi)REDIS_NODE_SLAVE標(biāo)識(shí),表示節(jié)點(diǎn)已從主節(jié)點(diǎn)變成從節(jié)點(diǎn)鞠呈。最后調(diào)用復(fù)制代碼開(kāi)始復(fù)制融师,這部分代碼就是單機(jī)模式下的復(fù)制代碼。

節(jié)點(diǎn)間的復(fù)制關(guān)系會(huì)通過(guò)消息發(fā)送給集群中的其它節(jié)點(diǎn)粟按,最后所有節(jié)點(diǎn)都會(huì)了解到其它節(jié)點(diǎn)間的復(fù)制關(guān)系诬滩,并保存在對(duì)應(yīng)的clusterNode結(jié)構(gòu)體中。

故障轉(zhuǎn)移

節(jié)點(diǎn)之間會(huì)定期向集群中其它節(jié)點(diǎn)發(fā)送PING消息檢測(cè)在線狀態(tài)灭将,如果對(duì)方在一定時(shí)間內(nèi)沒(méi)有回復(fù)PONG消息疼鸟,那么節(jié)點(diǎn)就會(huì)把對(duì)方標(biāo)為疑似下線REDIS_NODE_PFAIL狀態(tài)。各個(gè)節(jié)點(diǎn)之間會(huì)通過(guò)消息交換節(jié)點(diǎn)狀態(tài)庙曙,當(dāng)某個(gè)主節(jié)點(diǎn)收到其它主節(jié)點(diǎn)發(fā)來(lái)的下線報(bào)告時(shí)空镜,會(huì)把該報(bào)告存在目標(biāo)下線節(jié)點(diǎn)對(duì)應(yīng)的clusterNode結(jié)構(gòu)體的fail_reposts鏈表中。

下線報(bào)告

如果在下線報(bào)告鏈表中有半數(shù)以上的主節(jié)點(diǎn)都認(rèn)為某個(gè)主節(jié)點(diǎn)疑似下線捌朴,那么就把它標(biāo)記為已下線吴攒,并向集群廣播一條FAIL的消息,收到該消息的節(jié)點(diǎn)立刻也把該節(jié)點(diǎn)標(biāo)記為已下線砂蔽。

當(dāng)下線主節(jié)點(diǎn)的一個(gè)從節(jié)點(diǎn)收到FAIL消息后就開(kāi)始對(duì)下線主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移洼怔,步驟如下:

  1. 從節(jié)點(diǎn)會(huì)向集群廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST的消息,要求所有收到消息并有投票權(quán)(集群中負(fù)責(zé)處理slots的主節(jié)點(diǎn)才有投票權(quán))的主節(jié)點(diǎn)向該從節(jié)點(diǎn)投票左驾。
  2. 主節(jié)點(diǎn)收到投票請(qǐng)求后镣隶,如果在當(dāng)前配置紀(jì)元尚未投票給其它節(jié)點(diǎn),那么返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息投票支持诡右。
  3. 如果獲得半數(shù)以上主節(jié)點(diǎn)的支持票安岂,那么從節(jié)點(diǎn)就當(dāng)選為leader。
  4. Leader節(jié)點(diǎn)執(zhí)行SLAVEOF no one命令成為新的主節(jié)點(diǎn)帆吻,然后撤銷(xiāo)所有對(duì)已下線主節(jié)點(diǎn)的slot域那,并把這些slot指派給自己。
  5. 新的主節(jié)點(diǎn)向集群廣播一條PONG消息猜煮,讓其它節(jié)點(diǎn)意識(shí)它已經(jīng)成為了主節(jié)點(diǎn)次员,并接管了下線節(jié)點(diǎn)處理的slot。下線主節(jié)點(diǎn)的其它從節(jié)點(diǎn)會(huì)調(diào)整為復(fù)制新的主節(jié)點(diǎn)王带。
  6. 下線的主節(jié)點(diǎn)重新上線后會(huì)成為新主節(jié)點(diǎn)的從節(jié)點(diǎn)淑蔚。

參考/圖片出處:
1. 機(jī)械工業(yè)出版社 -《Redis設(shè)計(jì)與實(shí)現(xiàn)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辫秧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌被丧,老刑警劉巖盟戏,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绪妹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡柿究,警方通過(guò)查閱死者的電腦和手機(jī)邮旷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蝇摸,“玉大人婶肩,你說(shuō)我怎么就攤上這事∶蚕Γ” “怎么了律歼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)啡专。 經(jīng)常有香客問(wèn)我险毁,道長(zhǎng),這世上最難降的妖魔是什么们童? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任畔况,我火速辦了婚禮,結(jié)果婚禮上兼砖,老公的妹妹穿的比我還像新娘塑悼。我一直安慰自己擅这,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布吵瞻。 她就那樣靜靜地躺著,像睡著了一般覆积。 火紅的嫁衣襯著肌膚如雪听皿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天宽档,我揣著相機(jī)與錄音尉姨,去河邊找鬼。 笑死吗冤,一個(gè)胖子當(dāng)著我的面吹牛又厉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播椎瘟,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼覆致,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了肺蔚?” 一聲冷哼從身側(cè)響起煌妈,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后璧诵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體汰蜘,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年之宿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了族操。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡比被,死狀恐怖色难,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情等缀,我是刑警寧澤枷莉,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站项滑,受9級(jí)特大地震影響依沮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜枪狂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一危喉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧州疾,春花似錦辜限、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至颗胡,卻和暖如春毫深,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背毒姨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工哑蔫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弧呐。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓闸迷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親俘枫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腥沽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • NOSQL類(lèi)型簡(jiǎn)介鍵值對(duì):會(huì)使用到一個(gè)哈希表,表中有一個(gè)特定的鍵和一個(gè)指針指向特定的數(shù)據(jù)鸠蚪,如redis今阳,volde...
    MicoCube閱讀 3,981評(píng)論 2 27
  • 概述 Redis-Sentinel是Redis官方推薦的高可用性(HA)解決方案师溅,當(dāng)用Redis做Master-s...
    神秘者007閱讀 753評(píng)論 0 3
  • 如果自己走的路很長(zhǎng),請(qǐng)耐心走下去也許下一個(gè)拐角你能收獲喜悅 如果自己走的路很長(zhǎng)盾舌,不要?dú)怵H或許你的生活需要堅(jiān)持 如果...
    黎奈奈閱讀 247評(píng)論 0 0
  • 自上次參加訓(xùn)練營(yíng)半年過(guò)去了险胰,1月2月份連續(xù)參加了兩期,拿下了公眾號(hào)的原創(chuàng)矿筝,寫(xiě)了兩萬(wàn)多字,認(rèn)識(shí)了一群充滿(mǎn)激情的年輕人...
    米策閱讀 202評(píng)論 0 4
  • 母親沒(méi)什么文化棚贾,小學(xué)只念到三年級(jí)窖维,也沒(méi)出過(guò)遠(yuǎn)門(mén),幾十年只在小山村里跟著日升日落忙活妙痹。然而铸史,母親常常能說(shuō)出一些很...
    熱門(mén)神閱讀 291評(píng)論 1 4