淺析 Redis 主從復(fù)制實(shí)現(xiàn)原理

本篇主要分三部分討論Redis主從復(fù)制的實(shí)現(xiàn)原理:主從復(fù)制過程、狀態(tài)機(jī)、源碼解析。Redis從節(jié)點(diǎn)使用了狀態(tài)機(jī)機(jī)制,來實(shí)現(xiàn)從節(jié)點(diǎn)不同狀態(tài)的切換,所以在解析源碼之前,會(huì)先討論下狀態(tài)機(jī)的基本原理。

1. 主從復(fù)制過程

Redis 的 RDB 和 AOF 機(jī)制保證了服務(wù)的可靠性,而為了讓服務(wù)實(shí)現(xiàn)高可用,Redis 使用了主從復(fù)制,而主從復(fù)制也是MySQL等數(shù)據(jù)庫或其他存儲(chǔ)系統(tǒng)實(shí)現(xiàn)高可用的方法。

為了保證數(shù)據(jù)副本的一致性,主從庫之間采用的是讀寫分離的方式:

  • 讀操作:主庫、從庫都可以接收;
  • 寫操作:首先到主庫執(zhí)行,然后,主庫將寫操作同步給從庫。

1.1. 主從數(shù)據(jù)同步

當(dāng)我們啟動(dòng)多個(gè) Redis 實(shí)例的時(shí)候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關(guān)系。
例如,現(xiàn)在有實(shí)例 1(ip:172.16.19.3)和實(shí)例 2(ip:172.16.19.5),我們在實(shí)例 2 上執(zhí)行以下這個(gè)命令后,實(shí)例 2 就變成了實(shí)例 1 的從庫,并從實(shí)例 1 上復(fù)制數(shù)據(jù):

replicaof  172.16.19.3  6379

主從實(shí)例會(huì)按照三個(gè)階段完成數(shù)據(jù)同步:建立連接、全量復(fù)制、增量復(fù)制。下面對這三個(gè)階段進(jìn)行介紹:


1.png
  1. 建立連接:第一階段是主從庫間建立連接、協(xié)商同步的過程,主要是為全量復(fù)制做準(zhǔn)備。在這一步,從庫和主庫建立起連接,并告訴主庫即將進(jìn)行同步,主庫確認(rèn)回復(fù)后,主從庫間就可以開始同步了。這一步主要包含從庫給主庫發(fā)送 psync 命令,以及主庫響應(yīng) FULLRESYNC 命令。

    • 從庫發(fā)送 psync 命令:攜帶主庫runID 和 復(fù)制進(jìn)度 offset 兩個(gè)參數(shù)。因?yàn)閺膸斓谝淮芜B接主庫,并不知道主庫的 runID,因此這時(shí)候 runID = ?;而 offset 值為 -1,表示這是第一次復(fù)制。

    • 主庫響應(yīng) FULLRESYNC 命令:主庫 runID 和 目前的復(fù)制進(jìn)度 offset。FULLRESYNC 響應(yīng)表示接下來將會(huì)進(jìn)行全量復(fù)制。

  2. 全量復(fù)制:全量復(fù)制也會(huì)進(jìn)行兩個(gè)操作,主實(shí)例把所有的數(shù)據(jù)傳輸給從庫,從庫接收主庫的所有數(shù)據(jù),并在本地完成數(shù)據(jù)加載。具體來說就是:

    • 主庫執(zhí)行 bgsave 命令,生成 RDB 文件,接著將文件發(fā)給從庫。主實(shí)例在執(zhí)行 bgsave 命令時(shí),會(huì) fork 一個(gè)子進(jìn)程來生成 RDB 文件,這塊內(nèi)容后面再單獨(dú)討論吧。

    • 從庫接收到 RDB 文件后,會(huì)先清空當(dāng)前數(shù)據(jù)庫,然后加載 RDB 文件。這是因?yàn)閺膸煸谕ㄟ^ replicaof 命令開始和主庫同步前,可能保存了其他數(shù)據(jù)。為了避免之前數(shù)據(jù)的影響,從庫需要先把當(dāng)前數(shù)據(jù)庫清空。

  3. 增量復(fù)制:第二階段全量復(fù)制會(huì)是一個(gè)比較耗時(shí)的操作,而在進(jìn)行全量復(fù)制時(shí),主實(shí)例仍然在接收新的寫命令,而這些命令是不會(huì)被寫到 RDB 文件中的,具體為什么不會(huì)被寫到 RDB 文件中,可以參考http://www.itdecent.cn/p/f700dbd572a5 里面的 fork 和寫時(shí)復(fù)制相關(guān)技術(shù)。因此就需要 replication buffer 這樣一塊緩沖區(qū),來保存第二階段執(zhí)行期間,主實(shí)例接收的寫操作。并在第二階段執(zhí)行結(jié)束之后,把 replication buffer 緩沖區(qū)中的數(shù)據(jù)發(fā)送給從節(jié)點(diǎn)。

1.2. 網(wǎng)絡(luò)故障之后的數(shù)據(jù)同步

在 Redis 2.8 之前,如果主從庫在命令傳播時(shí)出現(xiàn)了網(wǎng)絡(luò)閃斷,那么,從庫就會(huì)和主庫重新進(jìn)行一次全量復(fù)制,開銷非常大。

從 Redis 2.8 開始,網(wǎng)絡(luò)斷了之后,主從庫會(huì)采用增量復(fù)制的方式繼續(xù)同步。聽名字大概就可以猜到它和全量復(fù)制的不同:全量復(fù)制是同步所有數(shù)據(jù),而增量復(fù)制只會(huì)把主從庫網(wǎng)絡(luò)斷連期間主庫收到的命令,同步給從庫。

具體過程如下圖所示:


2.png
  1. 當(dāng)主從庫斷連后,主庫會(huì)把斷連期間收到的寫操作命令,寫入 replication buffer,同時(shí)也會(huì)把這些操作命令也寫入 repl_backlog_buffer 這個(gè)緩沖區(qū)。repl_backlog_buffer 是一個(gè)環(huán)形緩沖區(qū),主庫會(huì)記錄自己寫到的位置master_repl_offset,從庫則會(huì)記錄自己已經(jīng)讀到的位置 slave_repl_offset
  2. 主從庫的連接恢復(fù)之后,從庫首先會(huì)給主庫發(fā)送 psync 命令,并把自己當(dāng)前的 slave_repl_offset 發(fā)給主庫,主庫只用把 master_repl_offset 和 slave_repl_offset 之間的命令操作同步給從庫就行。

因?yàn)?repl_backlog_buffer 是一個(gè)環(huán)形緩沖區(qū),所以在緩沖區(qū)寫滿后,主庫會(huì)繼續(xù)寫入,此時(shí),就會(huì)覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導(dǎo)致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會(huì)導(dǎo)致主從庫間的數(shù)據(jù)不一致。

2. 狀態(tài)機(jī)

在實(shí)際的軟件開發(fā)中,狀態(tài)模式并不是很常用,但是在能夠用到的場景里,它可以發(fā)揮很大的作用。狀態(tài)模式一般用來實(shí)現(xiàn)狀態(tài)機(jī),而狀態(tài)機(jī)常用在游戲、工作流引擎等系統(tǒng)開發(fā)中。

有限狀態(tài)機(jī),英文翻譯是 Finite State Machine,縮寫為 FSM,簡稱為狀態(tài)機(jī)。狀態(tài)機(jī)有 3 個(gè)組成部分:狀態(tài)(State)、事件(Event)、動(dòng)作(Action)。其中,事件也稱為轉(zhuǎn)移條件(Transition Condition)。事件觸發(fā)狀態(tài)的轉(zhuǎn)移及動(dòng)作的執(zhí)行。不過,動(dòng)作不是必須的,也可能只轉(zhuǎn)移狀態(tài),不執(zhí)行任何動(dòng)作。

拿《超級瑪麗》舉個(gè)例子哈,瑪麗可以有多種狀態(tài),比如小瑪麗,吃了蘑菇之后,就變成了大瑪麗并且增加100積分;而如果碰到了野怪,小瑪麗就直接over了,大瑪麗就會(huì)變成小瑪麗并且減少100積分。這個(gè)例子里面呢,小瑪麗或者大瑪麗都是狀態(tài)機(jī)的狀態(tài),加減積分就是動(dòng)作,吃蘑菇或者撞野怪就是事件。

3.png

簡化后的部分狀態(tài)和事件如下圖所示:
4.png

2.1. 分支實(shí)現(xiàn)

如果將上面描述的簡易版超級瑪麗用代碼實(shí)現(xiàn),簡單直接的實(shí)現(xiàn)方式是,參照狀態(tài)轉(zhuǎn)移圖,將每一個(gè)狀態(tài)轉(zhuǎn)移,原模原樣地直譯成代碼。這樣編寫的代碼會(huì)包含大量的 if-else 或 switch-case 分支判斷邏輯,甚至是嵌套的分支判斷邏輯。

public enum State {
  SMALL(0),
  SUPER(1),
  OVER(2);

  private int value;

  private State(int value) {
    this.value = value;
  }

  public int getValue() {
    return this.value;
  }
}

public class MarioStateMachine {
  private int score;
  private State currentState;

  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }

  public void obtainMushRoom() {
    if (currentState.equals(State.SMALL)) { 
        this.currentState = State.SUPER; 
        this.score += 100; 
    } else {
        this.score += 100; 
    }
  }

  public void meetMonster() {
    if (currentState.equals(State.SUPER)) { 
        this.currentState = State.SMALL; 
        this.score -= 100; 
    } else {
        this.currentState = State.OVEW; 
        this.score = 0;
    }
  }

  public int getScore() {
    return this.score;
  }

  public State getCurrentState() {
    return this.currentState;
  }
}

public class ApplicationDemo {
  public static void main(String[] args) {
    MarioStateMachine mario = new MarioStateMachine();
    mario.obtainMushRoom();
    int score = mario.getScore();
    State state = mario.getCurrentState();
    System.out.println("mario score: " + score + "; state: " + state);
  }
}

對于簡單的狀態(tài)機(jī)來說,分支邏輯這種實(shí)現(xiàn)方式是可以接受的。但是,對于復(fù)雜的狀態(tài)機(jī)來說,這種實(shí)現(xiàn)方式極易漏寫或者錯(cuò)寫某個(gè)狀態(tài)轉(zhuǎn)移。除此之外,代碼中充斥著大量的 if-else 或者 switch-case 分支判斷邏輯,可讀性和可維護(hù)性都很差。如果哪天修改了狀態(tài)機(jī)中的某個(gè)狀態(tài)轉(zhuǎn)移,我們要在冗長的分支邏輯中找到對應(yīng)的代碼進(jìn)行修改,很容易改錯(cuò),引入 bug。

2.2. 狀態(tài)模式

狀態(tài)模式通過將事件觸發(fā)的狀態(tài)轉(zhuǎn)移和動(dòng)作執(zhí)行,拆分到不同的狀態(tài)類中,來避免分支判斷邏輯。我們還是結(jié)合代碼來理解這句話。

其中,IMario 是狀態(tài)的接口,定義了所有的事件。SmallMario、SuperMario是 IMario 接口的實(shí)現(xiàn)類,分別對應(yīng)狀態(tài)機(jī)中的不同的狀態(tài)。原來在狀態(tài)機(jī)MarioStateMachine中定義的事件處理邏輯,現(xiàn)在分散到了 IMario 的實(shí)現(xiàn)類里面。

public interface IMario {
  State getName();
  void obtainMushRoom(MarioStateMachine stateMachine);
  void meetMonster(MarioStateMachine stateMachine);
}

public class SmallMario implements IMario {
  private static final SmallMario instance = new SmallMario();
  private SmallMario() {}
  public static SmallMario getInstance() {
    return instance;
  }

  @Override
  public State getName() {
    return State.SMALL;
  }

  @Override
  public void obtainMushRoom(MarioStateMachine stateMachine) {
    stateMachine.setCurrentState(SuperMario.getInstance());
    stateMachine.setScore(stateMachine.getScore() + 100);
  }

  @Override
  public void meetMonster(MarioStateMachine stateMachine) {
    stateMachine.setCurrentState(OverMario.getInstance());
  }
}

// 省略SuperMario類、OverMario類...
public class MarioStateMachine {
  private int score;
  private IMario currentState;

  public MarioStateMachine() {
    this.score = 0;
    this.currentState = SmallMario.getInstance();
  }

  public void obtainMushRoom() {
    this.currentState.obtainMushRoom(this);
  }

  public void meetMonster() {
    this.currentState.meetMonster(this);
  }

  public int getScore() {
    return this.score;
  }

  public State getCurrentState() {
    return this.currentState.getName();
  }

  public void setScore(int score) {
    this.score = score;
  }

  public void setCurrentState(IMario currentState) {
    this.currentState = currentState;
  }
}

其實(shí)狀態(tài)機(jī)還有一種實(shí)現(xiàn)方式為查表法,但是個(gè)人感覺這種查表法的應(yīng)用場景非常有限,這里就不詳細(xì)介紹了,有興趣可以看下極客時(shí)間里面王爭老師的專欄《設(shè)計(jì)模式之美》。

3. 源碼解析

Redis 5.0 源碼地址:https://github.com/redis/redis/tree/5.0

Redis 主從復(fù)制過程中,從節(jié)點(diǎn)會(huì)處于初始化、建立連接、握手驗(yàn)證、增量復(fù)制、全量復(fù)制等多個(gè)不同的狀態(tài)。Redis 就是使用了基于狀態(tài)機(jī)的設(shè)計(jì)思想,來清晰的實(shí)現(xiàn)不同狀態(tài)間的跳轉(zhuǎn)。因?yàn)橹鲝膹?fù)制過程中的狀態(tài)比較多,很難把每一個(gè)狀態(tài)都說清楚,這里只討論下關(guān)鍵的幾個(gè)狀態(tài)及狀態(tài)間的跳轉(zhuǎn)。

3.1. 數(shù)據(jù)結(jié)構(gòu)及初始化

每一個(gè) Redis 實(shí)例在代碼中都對應(yīng)一個(gè) redisServer 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體包含了和 Redis 實(shí)例相關(guān)的各種配置,比如實(shí)例的 RDB、AOF 配置、主從復(fù)制配置、切片集群配置等。然后,與主從復(fù)制狀態(tài)機(jī)相關(guān)的變量是 repl_state,Redis 在進(jìn)行主從復(fù)制時(shí),從庫就是根據(jù)這個(gè)變量值的變化,來實(shí)現(xiàn)不同階段的執(zhí)行和跳轉(zhuǎn)。

struct redisServer {
   ...
   /* 復(fù)制相關(guān)(slave) */
    char *masterauth;               /* 用于和主庫進(jìn)行驗(yàn)證的密碼*/
    char *masterhost;               /* 主庫主機(jī)名 */
    int masterport;                 /* 主庫端口號r */
    …
    client *master;        /* 從庫上用來和主庫連接的客戶端 */
    client *cached_master; /* 從庫上緩存的主庫信息 */
    int repl_state;          /* 從庫的復(fù)制狀態(tài)機(jī) */
   ...
}

個(gè)人理解這里的 repl_state 就相當(dāng)于 2.1 中的 State 枚舉類,定義了從庫的不同狀態(tài)。這里有一點(diǎn)需要說明哈,就是主從復(fù)制的狀態(tài)機(jī)都是在從節(jié)點(diǎn)上才有,主節(jié)點(diǎn)是沒有狀態(tài)機(jī)的,到后面會(huì)討論主節(jié)點(diǎn)為什么沒有狀態(tài)機(jī)這個(gè)問題。

接下來說下初始化,首先,當(dāng)一個(gè)實(shí)例啟動(dòng)后,就會(huì)調(diào)用 server.c 中的 initServerConfig 函數(shù),初始化 redisServer 結(jié)構(gòu)體。此時(shí),實(shí)例會(huì)把狀態(tài)機(jī)的初始狀態(tài)設(shè)置為 REPL_STATE_NONE,如下所示:

void initServerConfig(void) {
   …
   server.repl_state = REPL_STATE_NONE;
   …
}

然后,一旦實(shí)例執(zhí)行了 replicaof 172.16.19.3 6379 命令,就會(huì)調(diào)用 replication.c 中的 replicaofCommand 函數(shù)進(jìn)行處理。replicaofCommand 函數(shù)會(huì)調(diào)用 replicationSetMaster 函數(shù)設(shè)置主庫的信息。這部分的代碼邏輯如下所示:

void replicaofCommand(client *c) {
    /* The special host/port combination "NO" "ONE" turns the instance
     * into a master. Otherwise the new master address is set. */
    if (!strcasecmp(c->argv[1]->ptr,"no") &&
        !strcasecmp(c->argv[2]->ptr,"one")) {
        ......
    } else {
         /* 如果沒有記錄主庫的IP和端口號,設(shè)置主庫的信息 */
        replicationSetMaster(c->argv[1]->ptr, port);
        ......
    }
    addReply(c,shared.ok);
}

/* Set replication to the specified master address and port. */
void replicationSetMaster(char *ip, int port) {
    ......
    server.masterhost = sdsnew(ip);
    server.masterport = port;
    ......
    server.repl_state = REPL_STATE_CONNECT;
}

這里就是設(shè)置剛才數(shù)據(jù)結(jié)構(gòu)里面提到的 host 和 port 兩個(gè)變量,并把狀態(tài)機(jī)的狀態(tài)設(shè)置為 REPL_STATE_CONNECT

3.2. 狀態(tài)的跳轉(zhuǎn)

Redis 的周期性任務(wù),就是指 Redis 實(shí)例在運(yùn)行時(shí),按照一定時(shí)間周期重復(fù)執(zhí)行的任務(wù)。Redis 的周期性任務(wù)很多,其中之一就是 replicationCron() 任務(wù)。這個(gè)任務(wù)的執(zhí)行頻率是每 1000ms 執(zhí)行一次,如下面的代碼所示:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
   …
   run_with_period(1000) replicationCron();
   …
}

這個(gè) serverCron 函數(shù)在 server.c 函數(shù)中,他會(huì)在 Redis 實(shí)例啟動(dòng)的 main 函數(shù)執(zhí)行時(shí)候,注冊一個(gè)時(shí)間事件,該時(shí)間事件會(huì)立即被觸發(fā),觸發(fā)后的回調(diào)函數(shù)就是這個(gè) serverCron 函數(shù)。這一塊的詳細(xì)內(nèi)容,后面疫情過去到公司了,再把事件驅(qū)動(dòng)框架的分析貼出來,這里只需要知道這個(gè)函數(shù)會(huì)在 Redis 啟動(dòng)之后執(zhí)行,并按照一定周期來執(zhí)行相應(yīng)的任務(wù)就行
接下來再來看下 replicationCron 函數(shù),他是在 replication.c 文件中,在這個(gè)函數(shù)里面,判斷從節(jié)點(diǎn)狀態(tài)機(jī)的狀態(tài)為 REPL_STATE_CONNECT 時(shí),會(huì)和主節(jié)點(diǎn)建立連接,如下所示:

/* Replication cron function, called 1 time per second. */
void replicationCron(void) {
    ......
    /* Check if we should connect to a MASTER */
    /* 如果從庫實(shí)例的狀態(tài)是REPL_STATE_CONNECT,那么從庫通過connectWithMaster和主庫建立連接 */
    //3.1 小節(jié)中有分析過,執(zhí)行了replicaof 之后,會(huì)把從庫的狀態(tài)機(jī)設(shè)置為 REPL_STATE_CONNECT,因此就會(huì)首先執(zhí)行這個(gè)分支
    if (server.repl_state == REPL_STATE_CONNECT) {
        serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
            server.masterhost, server.masterport);
        if (connectWithMaster() == C_OK) {
            serverLog(LL_NOTICE,"MASTER <-> REPLICA sync started");
        }
    }
    ......
}

connectWithMaster 函數(shù)中,首先和主節(jié)點(diǎn)建立連接,返回一個(gè)文件描述符 fd,當(dāng)連接 fd 上有事件發(fā)生時(shí),會(huì)觸發(fā) syncWithMaster 回調(diào)函數(shù),方法返回前,會(huì)給狀態(tài)機(jī)的狀態(tài)設(shè)置為 REPL_STATE_CONNECTING

int connectWithMaster(void) {
    int fd;
    //從庫和主庫建立連接
    fd = anetTcpNonBlockBestEffortBindConnect(NULL,
        server.masterhost,server.masterport,NET_FIRST_BIND_ADDR);
    if (fd == -1) {
        serverLog(LL_WARNING,"Unable to connect to MASTER: %s",
            strerror(errno));
        return C_ERR;
    }
    //在建立的連接上注冊讀寫事件,對應(yīng)的回調(diào)函數(shù)是syncWithMaster
    if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) ==
            AE_ERR)
    {
        close(fd);
        serverLog(LL_WARNING,"Can't create readable event for SYNC");
        return C_ERR;
    }

    server.repl_transfer_lastio = server.unixtime;
    server.repl_transfer_s = fd;
    //完成連接后,將狀態(tài)機(jī)設(shè)置為REPL_STATE_CONNECTING
    server.repl_state = REPL_STATE_CONNECTING;
    return C_OK;
}

syncWithMaster 函數(shù)前面會(huì)經(jīng)過一系列的握手操作,然后會(huì)調(diào)用 slaveTryPartialResynchronization 函數(shù)發(fā)送 1.1 小節(jié)中提到的 psync 命令,并根據(jù) slaveTryPartialResynchronization 函數(shù)的返回值,來執(zhí)行全量復(fù)制,或者讓 slaveTryPartialResynchronization 函數(shù)執(zhí)行增量復(fù)制

void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
    //前面有一系列握手操作,這里就不詳細(xì)介紹了
    ......
    if (server.repl_state == REPL_STATE_SEND_PSYNC) {
        //向主庫發(fā)送PSYNC命令,進(jìn)行數(shù)據(jù)同步
        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;
    }
    
    //讀取PSYNC命令的返回結(jié)果
    psync_result = slaveTryPartialResynchronization(fd,1);
    //PSYNC結(jié)果還沒有返回,先從syncWithMaster函數(shù)返回處理其他操作
    if (psync_result == PSYNC_WAIT_REPLY) return;
    //如果PSYNC結(jié)果是PSYNC_CONTINUE,從syncWithMaster函數(shù)返回
    if (psync_result == PSYNC_CONTINUE) {
           …
           return;
    }

    //如果執(zhí)行全量復(fù)制的話,針對連接上的讀事件,創(chuàng)建readSyncBulkPayload回調(diào)函數(shù)
    if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL)
                == AE_ERR)
        {
           …
        }
    //將從庫狀態(tài)機(jī)置為REPL_STATE_TRANSFER
    server.repl_state = REPL_STATE_TRANSFER;


}

下面簡單介紹下 slaveTryPartialResynchronization 函數(shù),如果從庫是第一次和主庫連接,則發(fā)送 psync 命令,然后讀取主庫的響應(yīng),并根據(jù)主庫的響應(yīng)結(jié)果,來執(zhí)行增量復(fù)制:

int slaveTryPartialResynchronization(int fd, int read_reply) {
    ......

    /* Writing half */
    //發(fā)送PSYNC命令,
    if (!read_reply) {
        ......
         //從庫第一次和主庫同步時(shí),設(shè)置offset為-1
        server.master_initial_offset = -1;

        ......
        /* Issue the PSYNC command */
        //調(diào)用sendSynchronousCommand發(fā)送PSYNC命令
        reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_replid,psync_offset,NULL);
        ......
        return PSYNC_WAIT_REPLY;
    }

    /* Reading half */
    //讀取主庫響應(yīng)
    reply = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
    if (sdslen(reply) == 0) {
        /* The master may send empty newlines after it receives PSYNC
         * and before to reply, just to keep the connection alive. */
        sdsfree(reply);
        return PSYNC_WAIT_REPLY;
    }

    aeDeleteFileEvent(server.el,fd,AE_READABLE);

    //主庫返回FULLRESYNC
    if (!strncmp(reply,"+FULLRESYNC",11)) {
        ......
        return PSYNC_FULLRESYNC;
    }
    //主庫返回CONTINUE,執(zhí)行增量復(fù)制
    if (!strncmp(reply,"+CONTINUE",9)) {
        ......
        return PSYNC_CONTINUE;
    }

    ......
    return PSYNC_NOT_SUPPORTED;
}

3.3. 主庫的操作

在 Redis 實(shí)現(xiàn)主從復(fù)制時(shí),從庫涉及到的狀態(tài)變遷有很多,包括了發(fā)起連接、主從握手、復(fù)制類型判斷、請求數(shù)據(jù)等。因此,使用狀態(tài)機(jī)開發(fā)從庫的復(fù)制流程,可以很好地幫助我們實(shí)現(xiàn)狀態(tài)流轉(zhuǎn)。

主從復(fù)制的發(fā)起方是從庫,而對于主庫來說,它只是被動(dòng)式地響應(yīng)從庫的各種請求,并根據(jù)從庫的請求執(zhí)行相應(yīng)的操作,比如生成 RDB 文件或是傳輸數(shù)據(jù)等。

而且,從另外一個(gè)角度來說,主庫可能和多個(gè)從庫進(jìn)行主從復(fù)制,而不同從庫的復(fù)制進(jìn)度和狀態(tài)很可能并不一樣,如果主庫要維護(hù)狀態(tài)機(jī)的話,那么,它還需要為每個(gè)從庫維護(hù)一個(gè)狀態(tài)機(jī),這個(gè)既會(huì)增加開發(fā)復(fù)雜度,也會(huì)增加運(yùn)行時(shí)的開銷。正是因?yàn)檫@些原因,所以主庫并不需要使用狀態(tài)機(jī)進(jìn)行狀態(tài)流轉(zhuǎn)。

主庫本身是可能發(fā)生故障,并要進(jìn)行故障切換的。如果主庫在執(zhí)行主從復(fù)制時(shí),也維護(hù)狀態(tài)機(jī),那么一旦主庫發(fā)生了故障,也還需要考慮狀態(tài)機(jī)的冗余備份和故障切換,這會(huì)給故障切換的開發(fā)和執(zhí)行帶來復(fù)雜度和開銷。而從庫維護(hù)狀態(tài)機(jī)本身就已經(jīng)能完成主從復(fù)制,所以沒有必要讓主庫再維護(hù)狀態(tài)機(jī)了。

參考資料:

  1. 極客時(shí)間專欄《Redis源碼剖析與實(shí)戰(zhàn)》.蔣德鈞.2021
  2. 極客時(shí)間專欄《Redis核心技術(shù)與實(shí)戰(zhàn)》.蔣德鈞.2020
  3. 極客時(shí)間專欄《設(shè)計(jì)模式之美》.王爭.2020
  4. Redis 5.0.14 源碼:https://github.com/redis/redis/tree/5.0

`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容