Redis源码解析:16Resis主从复制之主节点的完全重同步流程

         主从复制过程中,主节点根据从节点发来的命令执行相应的操作。结合上一章中讲解的从节点在主从复制中的流程,本章以及下一篇文章讲解一下主节点在主从复制过程中的流程。

         本章主要介绍完全重同步流程。

 

一:从节点建链和握手

         从节点在向主节点发起TCP建链,以及复制握手过程中,主节点一直把从节点当成一个普通的客户端处理。也就是说,不为从节点保存状态,只是收到从节点发来的命令进而处理并回复罢了。

         从节点在握手过程中第一个发来的命令是”PING”,主节点调用redis.c中的pingCommand函数处理,只是回复字符串”+PONG”即可。

 

         接下来从节点向主节点发送”AUTHxxx”命令进行认证,主节点调用redis.c中的authCommand函数进行处理,该函数的代码如下:

void authCommand(redisClient *c) {
    if (!server.requirepass) {
        addReplyError(c,"Client sent AUTH, but no password is set");
    } else if (!time_independent_strcmp(c->argv[1]->ptr, server.requirepass)) {
      c->authenticated = 1;
      addReply(c,shared.ok);
    } else {
      c->authenticated = 0;
      addReplyError(c,"invalid password");
    }
}

         server.requirepass根据配置文件中"requirepass"的选项进行设置,保存了Redis实例的密码。如果该值为NULL,说明本Redis实例不需要密码。这种情况下,如果从节点发来”AUTH xxx”命令,则回复给从节点错误信息:"Client sent AUTH, but no password is set"。

         接下来,对从节点发来的密码和server.requirepass进行比对,如果匹配成功,则回复给客户端”+OK”,否则,回复给客户端错误信息:"invalid password"。

 

         从节点接下来发送"REPLCONF listening-port  <port>"和"REPLCONF capa  eof"命令,告知主节点自己的监听端口和“能力”。主节点通过replication.c中的replconfCommand函数处理这些命令,代码如下:

void replconfCommand(redisClient *c) {
    int j;

    if ((c->argc % 2) == 0) {
        /* Number of arguments must be odd to make sure that every
         * option has a corresponding value. */
        addReply(c,shared.syntaxerr);
        return;
    }

    /* Process every option-value pair. */
    for (j = 1; j < c->argc; j+=2) {
        if (!strcasecmp(c->argv[j]->ptr,"listening-port")) {
            long port;

            if ((getLongFromObjectOrReply(c,c->argv[j+1],
                    &port,NULL) != REDIS_OK))
                return;
            c->slave_listening_port = port;
        } else if (!strcasecmp(c->argv[j]->ptr,"capa")) {
            /* Ignore capabilities not understood by this master. */
            if (!strcasecmp(c->argv[j+1]->ptr,"eof"))
                c->slave_capa |= SLAVE_CAPA_EOF;
        } 
	   ...
    }
    addReply(c,shared.ok);
}

         “REPLCONF”命令的格式为"REPLCONF  <option>  <value> <option>  <value>  ..."。因此,如果命令参数是偶数,说明命令格式错误,回复给从节点客户端错误信息:"-ERR syntax error";

         如果从节点发来的是"REPLCONF listening-port  <port>"命令,则从中取出<port>信息,保存在客户端的slave_listening_port属性中,记录从节点客户端的监听端口,主节点使用从节点的IP地址和监听端口,作为从节点的身份标识;

         如果从节点发来的是"REPLCONF capa  eof"命令,则将从节点客户端的能力属性slave_capa增加SLAVE_CAPA_EOF标记,表示该从节点支持无硬盘复制。目前为止,仅有这一种能力标记。

 

二:完全重同步时,从节点状态转换

         接下来,从节点会向主节点发送”SYNC”或”PSYNC”命令,请求进行完全重同步或者部分重同步。

         主节点收到这些命令之后,如果是需要进行完全重同步,则开始在后台进行RDB数据转储(将数据保存在本地文件或者直接发给从节点)。同时,在前台接着接收客户端发来的命令请求。为了使从节点能与主节点的状态保持一致,主节点需要将这些命令请求缓存起来,以便在从节点收到主节点RDB数据并加载完成之后,将这些累积的命令流发送给从节点。

 

         从收到从节点的”SYNC”或”PSYNC”命令开始,主节点开始为该从节点保存状态。从此时起,站在主节点的角度,从节点的状态会发生转换。

         主节点为从节点保存的状态记录在客户端结构redisClient中的replstate属性中。从主节点的角度看,从节点需要经历的状态分别是:REDIS_REPL_WAIT_BGSAVE_START、REDIS_REPL_WAIT_BGSAVE_END、REDIS_REPL_SEND_BULK和REDIS_REPL_ONLINE。

 

         当主节点收到从节点发来的”SYNC”或”PSYNC”命令,并且需要完全重同步时,将从节点的状态置为REDIS_REPL_WAIT_BGSAVE_START,表示该从节点等待主节点后台RDB数据转储的开始;

 

         接下来,当主节点开始在后台进行RDB数据转储时,将从节点的状态置为REDIS_REPL_WAIT_BGSAVE_END,表示该从节点等待主节点后台RDB数据转储的完成;

         主节点在后台进行RDB数据的转储的时候,依然可以接收客户端发来的命令请求,为了能使从节点与主节点保持一致,主节点需要将客户端发来的命令请求,保存到从节点客户端的输出缓存中,这就是所谓的为从节点累积命令流。当从节点的复制状态变为REDIS_REPL_ONLINE时,就可以将这些累积的命令流发送个从节点了。

 

         如果主节点在进行后台RDB数据转储时,使用的是有硬盘复制的方式(将RDB数据保存在本地文件),则RDB数据转储完成时,将从节点的状态置为REDIS_REPL_SEND_BULK,表示接下来要将本地的RDB文件发送给客户端了;当所有的RDB数据发送完成后,将从节点的状态置为REDIS_REPL_ONLINE,表示可以向从节点发送累积的命令流了。

         如果主节点在进行后台RDB数据转储时,使用的是无硬盘复制的方式(将RDB数据直接通过网络发送给从节点),则RDB数据发送完成之后,收到从节点发来的第一个"REPLCONF  ACK  <offset>"后,就将从节点的状态置为REDIS_REPL_ONLINE,表示可以向从节点发送累积的命令流了。

         无硬盘复制的RDB数据转储,之所以要等到收到第一个"REPLCONF  ACK  <offset>"后,才能将从节点的状态置为REDIS_REPL_ONLINE。结合注释:https://github.com/antirez/redis/commit/bb7fea0d5ca7b3a53532338e8654e409014c1194,个人理解是因为无硬盘复制的RDB数据,不同于有硬盘复制的RDB数据,它没有长度标记,从节点每次从socket读取的数据量都是固定的(4k)。下面是从节点读取RDB数据时调用的readSyncBulkPayload函数中,每次read之前,计算要读取多少字节的代码,usemark为1表示无硬盘复制:

    /* Read bulk data */
    if (usemark) {
        readlen = sizeof(buf);
    } else {
        left = server.repl_transfer_size - server.repl_transfer_read;
        readlen = (left < (signed)sizeof(buf)) ? left : (signed)sizeof(buf);
    }

         因此,主节点在通过socket发送完RDB数据之后,如果接着就使用该socket发送累积的命令流,则从节点读取数据时,最后读到的数据中,有可能一部分是RDB数据,剩下的部分是累积的命令流。而此时从节点接下来就要加载RDB数据,无法处理这部分累积的命令流,只能丢掉,这就造成了主从数据库状态不一致了。

         因此,等到从节点发来第一个"REPLCONF  ACK <offset>"消息之后,此时能保证从节点已经加载完RDB数据,可以接收累积的命令流了。因此,这时才可以将从节点的复制状态置为REDIS_REPL_ONLINE。

         有硬盘复制的RDB数据,因为数据头中包含了数据长度,因此从节点知道总共需要读取多少RDB数据。因此,有硬盘复制的RDB数据转储,在发送完RDB数据之后,就可以立即将从节点复制状态置为REDIS_REPL_ONLINE。

 

         根据以上的描述,总结从节点的状态转换图如下: 


三:SYNC或PSYNC命令的处理

         主节点收到从节点发来的”SYNC”或”PSYNC”命令后,如果需要为该从节点进行完全重同步,将从节点的复制状态置为REDIS_REPL_WAIT_BGSAVE_START。开始在后台进行RDB数据转储时,则将复制状态置为REDIS_REPL_WAIT_BGSAVE_END。

 

         这里有一个问题,考虑这样一种情形:当主节点收到从节点A的”SYNC”或”PSYNC”命令后,要为该从节点进行完全重同步时,在将A的复制状态变为REDIS_REPL_WAIT_BGSAVE_END时刻起,主节点在前台接收客户端的命令请求,将该命令情求保存到A的输出缓存中,并在后台进行有硬盘复制的RDB数据转储。

         在后台进行有硬盘复制的RDB数据转储尚未完成时,如果又有新的从节点B发来了”SYNC”或”PSYNC”命令,同样需要完全重同步。此时主节点后台正在进行RDB数据转储,而且已经为A缓存了命令流。那么从节点B完全可以重用这份RDB数据,而无需再执行一次RDB转储了。而且将A中的输出缓存复制到B的输出缓存中,就能保证B的数据库状态也能与主节点一致了。因此,直接将B的复制状态直接置为REDIS_REPL_WAIT_BGSAVE_END,等到后台RDB数据转储完成时,直接将该转储文件同时发送给从节点A和B即可。

         但是如果此刻主节点进行的是无硬盘复制的RDB数据转储,这意味着主节点是直接将RDB数据通过socket发送给从节点A的,从节点B也就无法重用RDB数据了,因此需要再次执行一次BGSAVE操作。

 

         下面就是主节点收到”SYNC”或”PSYNC”命令的处理函数syncCommand的代码:

void syncCommand(redisClient *c) {
    /* ignore SYNC if already slave or in monitor mode */
    if (c->flags & REDIS_SLAVE) return;

    /* Refuse SYNC requests if we are a slave but the link with our master
     * is not o
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值