Redis主從復(fù)制

為了提高性能和系統(tǒng)可用,Redis都會做主從復(fù)制,一來可以分擔(dān)主庫壓力,二來在主庫掛掉的時候從庫依舊可以提供服務(wù)。Redis的主從復(fù)制是異步復(fù)制,返回結(jié)果給客戶端和同步命令到從庫是兩回事,互不相干,主庫也不關(guān)心從庫的執(zhí)行結(jié)果,對于同步命令執(zhí)行的結(jié)果,從庫會直接丟棄并不返回給主庫。Redis的主從復(fù)制簡單高效,但也不太算可靠。

Redis的主從復(fù)制是異步復(fù)制;全量同步(或增量同步)+命令傳播

Slave Server

Slave Server啟動初始化配置,根據(jù)slaveof配置設(shè)置Slave Server的主庫host(masterhost)和Slave Server的同步狀態(tài)(repl_state),和所有Server一樣監(jiān)聽客戶端鏈接,開啟后臺任務(wù)。

后臺定時任務(wù)包含,觸發(fā)AOF重寫、RDB快照、redis監(jiān)控、狀態(tài)收集、主從同步相關(guān)定時任務(wù)等

主從同步后臺定時任務(wù)包含,從庫連接主庫、從庫重連主庫、從庫給主庫發(fā)送同步進(jìn)度、主庫向從庫發(fā)送心跳包、主庫刪除超時從庫、主庫清除同步緩沖區(qū)、主庫刷新從庫狀態(tài)等

 

從庫連接主庫

從庫鏈接主庫后,開啟同步前的準(zhǔn)備和交互,同時從庫伴隨著和主庫交互變換自身狀態(tài)。下面源代碼看一下整個流程(代碼有刪減)

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {    /*
     * 從庫和主庫tcp握手成功后,從庫的同步狀態(tài)由REPL_STATE_CONNECT => REPL_STATE_CONNECTING
     * 從庫向主庫發(fā)送PING確保可以進(jìn)行同步操作
     * 發(fā)送PING,等待接收PONG     */ 
    if (server.repl_state == REPL_STATE_CONNECTING) {        // 修改同步狀態(tài) REPL_STATE_CONNECTING => REPL_STATE_RECEIVE_PONG
        server.repl_state = REPL_STATE_RECEIVE_PONG;
        err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PING",NULL);
    }    /*
     * 接收PONG響應(yīng),并修改狀態(tài) REPL_STATE_RECEIVE_PONG => REPL_STATE_SEND_AUTH     */
    if (server.repl_state == REPL_STATE_RECEIVE_PONG) {
        err = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
        server.repl_state = REPL_STATE_SEND_AUTH;
    }    /*
     * 以下有多個類似分支,例如:
     * 發(fā)送身份驗(yàn)證信息
     * 從庫同步的端口、IP等信息給主庫
     * 就跳過不羅列了,直接到 同步命令的分支     */
    /*
     * 從庫會先嘗試增量同步(發(fā)送psync命令+當(dāng)前同步的進(jìn)度repl_offset),這種情況是在從庫和主庫網(wǎng)絡(luò)閃斷后進(jìn)行;
     * 換句話說,如果從庫第一次連接主庫,那么增量同步是不存在的(畢竟你之前就沒有同步過,哪來的增量?。?
     * 還有一種情況,如果從庫同步進(jìn)度落后主庫同步緩沖區(qū)(repl_backlog)太多,也會進(jìn)行全量同步,具體后話說明     */ 
    if (server.repl_state == REPL_STATE_SEND_PSYNC) {        // 發(fā)送增量同步請求命令
        if (slaveTryPartialResynchronization(fd,0) == PSYNC_WRITE_ERROR) {
            err = sdsnew("Write error sending the PSYNC command.");            goto write_error;
        }
        server.repl_state = REPL_STATE_RECEIVE_PSYNC;        return;
    }    /*
     * 接收增量同步請求結(jié)果     */
    psync_result = slaveTryPartialResynchronization(fd,1);    if (psync_result == PSYNC_WAIT_REPLY) return; /* Try again later... */
    /*
     * 不支持增量同步,即不支持psync命令,Redis2.8以上才支持
     * 就進(jìn)行全量同步,發(fā)送sync命令     */
    if (psync_result == PSYNC_NOT_SUPPORTED) {        // 發(fā)送sync命令
        if (syncWrite(fd,"SYNC\r\n",6,server.repl_syncio_timeout*1000) == -1) {
        }
    }    /*
     * 發(fā)送完同步命令后,回調(diào)readSyncBulkPayload方法獲取主庫回復(fù)同步數(shù)據(jù)     */ 
    if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL)            == AE_ERR)
    {
    }    /*
     * 修改同步狀態(tài)為REPL_STATE_TRANSFER,即同步數(shù)據(jù)傳輸狀態(tài)     */ 
    server.repl_state = REPL_STATE_TRANSFER;    return;
}

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

以上代碼有點(diǎn)長,總結(jié)一下步驟:1)slave發(fā)送自身信息到master;2)嘗試增量同步,成功則等待master回送同步數(shù)據(jù);3)不支持增量同步或者增量同步失敗,則進(jìn)行全量同步,并等待master回送同步數(shù)據(jù)。

全量同步

1、從庫發(fā)送sync命令給主庫,發(fā)起全量同步,并等待數(shù)據(jù)返回

2、主庫接收到sync命令,把內(nèi)存數(shù)據(jù)保存到rdb文件并把文件內(nèi)容發(fā)送給從庫

3、從庫接收同步數(shù)據(jù)并保存到本地rdb文件,最后把rdb文件內(nèi)容寫入到內(nèi)存數(shù)據(jù)庫,至此全量同步完成

以上是簡述一下同步流程,但其中并不止那么簡單,例如:同時多個slave發(fā)起全量同步請求,主庫也只會進(jìn)行一次bgsave,保存內(nèi)存快照到rdb文件。

rdb是redis持久化的一種,是當(dāng)前redis內(nèi)存數(shù)據(jù)的快照。所以全量同步,主庫發(fā)送給從庫的是,是內(nèi)存中每一個key和它對應(yīng)的value(key => value)。

 

以下源碼分析第三步操作(代碼有刪減)

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {    // 讀取第一行,獲取同步數(shù)據(jù)量的大小
    if (server.repl_transfer_size == -1) {        if (strncmp(buf+1,"EOF:",4) == 0 && strlen(buf+5) >= CONFIG_RUN_ID_SIZE) {
        } else {            // 同步數(shù)據(jù)量的大小
            server.repl_transfer_size = strtol(buf+1,NULL,10);
        }        return;
    }    // 讀取數(shù)據(jù)
    nread = read(fd,buf,readlen);    // 更新最后同步通信時間
    server.repl_transfer_lastio = server.unixtime;    // 保存數(shù)據(jù)到本地rdb文件
    if (write(server.repl_transfer_fd,buf,nread) != nread) {
    }    if (eof_reached) {        // 從rdb文件中載入數(shù)據(jù)到內(nèi)存
        serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory");        if (rdbLoad(server.rdb_filename) != C_OK) {
        }        // 同步完后,slave修改主庫信息,開始接收主庫命令擴(kuò)散        replicationCreateMasterClient(server.repl_transfer_s);
        serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
    }    return;
}/*
 * 同步完后,slave修改主庫信息,開始接收主庫命令擴(kuò)散 */void replicationCreateMasterClient(int fd) {
    server.master = createClient(fd);  // 這里設(shè)置監(jiān)聽接收主庫的發(fā)送的數(shù)據(jù)
    server.master->flags |= CLIENT_MASTER;
    server.master->authenticated = 1; 
    server.repl_state = REPL_STATE_CONNECTED;  // 修改從庫同步狀態(tài)
    /*
     * reploff和replrunid這兩個是增量同步的必要參數(shù)     */
    server.master->reploff = server.repl_master_initial_offset; // master的同步緩沖區(qū)的總量(當(dāng)前從庫同步進(jìn)度)
    memcpy(server.master->replrunid, server.repl_master_runid,        sizeof(server.repl_master_runid));   // 保存主庫runid}

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

命令傳播

完成全量同步(增量同步)后,主庫接受客戶端命令,修改了數(shù)據(jù),為了保持主從數(shù)據(jù)一致,這些命令也需要在從庫上執(zhí)行一遍,哪怎么操作呢?當(dāng)然不是客戶端逐一修改所有從庫了,而是由主庫執(zhí)行命令成功后,異步地把命令發(fā)送給所有從庫。

這部分主要工作在于主庫,就不說了,下一篇主庫角度再說。從庫接收主庫的命令傳播,其實(shí)和其他客戶端的命令一樣的行為,只是從庫會判斷是否來自主庫發(fā)送的命令,更新自己同步進(jìn)度,主庫不關(guān)心從庫執(zhí)行命令的結(jié)果,所以從庫也不會發(fā)送執(zhí)行結(jié)果給主庫,省略了一次網(wǎng)絡(luò)IO。

心跳檢測

開篇我們說過,Redis會有個定時任務(wù)在后臺執(zhí)行,從庫會每秒向主庫發(fā)送ack+同步進(jìn)度;主庫也會定時發(fā)送PING命令檢測它所有從庫的存活。

 

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

/*
 * 從庫會每秒向主庫發(fā)送ack+同步進(jìn)度
 * psync ack repl_off */if (server.masterhost && server.master &&
    !(server.master->flags & CLIENT_PRE_PSYNC))
    replicationSendAck();/*
 * 主庫定時發(fā)送PING命令檢測它所有從庫的存活 */if ((replication_cron_loops % server.repl_ping_slave_period) == 0) {
    ping_argv[0] = createStringObject("PING",4);
    replicationFeedSlaves(server.slaves, server.slaveseldb,
        ping_argv, 1);
    decrRefCount(ping_argv[0]);
}

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營銷培訓(xùn)

風(fēng)險

Redis的主從復(fù)制是很高效,也沒有太多花哨的東西,基于異步同步,客戶端不需要等待同步結(jié)果,但是也是這樣的高效同步帶來一些風(fēng)險。

1)主庫發(fā)送僅且發(fā)送一次命令給從庫,如果超時,命令丟失,從庫沒有接收到,會造成不一致;

2)從庫內(nèi)存滿了,主庫也沒法知道,而且從庫收到命令傳播依舊會更新自己同步進(jìn)度;

3)異步復(fù)制帶來的同步間隙,造成短時間內(nèi)不一致,這點(diǎn)要根據(jù)具體業(yè)務(wù)處理;

4)客戶端修改從庫數(shù)據(jù),也會導(dǎo)致主從不一致,可以把從庫設(shè)置成只讀;

廢話

從庫未全量同步過

Slave:Master,我要增量同步(psync ? -1)

Master:EXO ME?你沒全量同步過,不行,你必須全量同步

Slave:好的,師父。全量同步(Sync)

Master:同意。等著吧。

(Slave等啊等)

Master:接著,rdb

Slave:收!

(Slave全量同步完后,上線工作了,命令傳播)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

……

主從網(wǎng)絡(luò)斷開重連后

Slave:師父,剛掉線了,我是不是錯過了什么,我要增量同步一下(psync Master_id repl_off)

Master:剛哪浪去了?問你又不答我。行了,增量同步一下吧,等著

Slave:好的,師父。

(Slave等啊等)

Master:接著,這些都是你剛沒有執(zhí)行的命令

Slave:收!

(Slave增量同步完后,上線工作了,命令傳播)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

……

主從網(wǎng)絡(luò)斷開重連后,slave落后太多

Slave:師父,剛掉線了,我是不是錯過了什么,我要增量同步一下(psync Master_id repl_off)

Master:剛哪浪去了?問你又不答我。不行不行,你落后了(repl_off進(jìn)度太舊了),你先全量一下我們在聊吧

Slave:好的,師父。全量同步(Sync)

Master:同意。等著吧。

(Slave等啊等)

Master:接著,rdb

Slave:收!

(Slave全量同步完后,上線工作了,命令傳播)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

Master:這是我剛執(zhí)行完的命令,你執(zhí)行一下

Slave:好的(執(zhí)行完了也不告訴你)

……

http://www.cnblogs.com/szuyuan/p/7197437.html