Redis源码剖析——主从复制(3)

相关文章

Redis源码剖析——主从复制(1)
Redis源码剖析——主从复制(2)
Redis源码剖析——主从复制(4)
Redis源码剖析——主从复制(5)—共享复制缓冲区的方案

目录

主从复制——主节点

一、主节点属性

二、建立连接与同步流程

1.从节点建链和握手

2.完全重同步时,从节点状态转换

 3.PSYNC命令的处理

 4.为从节点累积命令流

5.BGSAVE操作完成

总结


主从复制——主节点

从主节点角度出发,分析在Redis源码中的主从复制部分。

一、主节点属性

server.slaveseldb:主从复制时当前正在使用的数据库

server.master_repl_offset:全局复制偏移量(一个累计值)

server.slaves:保存了所有从服务器的链表(列表节点为 struct redisClient)

server.repl_backlog:部分同步的复制积压 backlog 本身    

server.repl_backlog_size:backlog 的长度

server.repl_backlog_histlen:backlog 中数据的长度

server.repl_backlog_idx:backlog 的当前索引  

server.repl_backlog_off: backlog 中可以被还原的第一个字节的偏移量

server.repl_ping_slave_period:主服务器向从节点输出缓存以及积压队列中发送 PING 的频率

server.repl_backlog_time_limit:当主节点没有从节点连接时,backlog最长的存活时间,该值默认为1个小时

server.repl_no_slaves_since:该主节点已经没有连接的从节点时,会设置该属性为当时的时间戳,用于计算比较backlog的存活时间

server.repl_min_slaves_to_write:是否开启最小数量从服务器写入功能,至少有N个从机才能写入数据。保证从机最低数量。 

server.repl_min_slaves_max_lag:定义最小数量从服务器的最大延迟值,如果每个从机的延迟值大于N,则拒绝写入数据。保证主从同步延迟。

server.repl_good_slaves_count:延迟状态良好的从服务器的数量 

server.stat_sync_partial_ok:部分重同步 PSYNC 成功执行的次数

server.stat_sync_partial_err:部分重同步 PSYNC 执行失败的次数

server.stat_sync_full:执行完整重同步的次数

server.requirepass:是否设置了密码

二、建立连接与同步流程

1.从节点建链和握手

从节点在向主节点发起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");
    // 主节点需要密码认证,且密码匹配成功,从节点c->authenticated置1
    } else if (!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"。如果设置了requirepass选项,接下来,对从节点发来的密码和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) {
        addReply(c,shared.syntaxerr);
        return;
    }
    /* 处理每个参数选项对 */
    for (j = 1; j < c->argc; j+=2) {
        // 从服务器发来 REPLCONF listening-port <port> 命令
        // 主服务器将从服务器监听的端口号记录下来
        if (!strcasecmp(c->argv[j]->ptr,"listening-port")) {
            long port;
            //将参数中从服务器的端口号写入从服务器对应客户端的c->slave_listening_port中
            if ((getLongFromObjectOrReply(c,c->argv[j+1],
                    &port,NULL) != REDIS_OK))
                return;
            c->slave_listening_port = port;
        // 从服务器发来 REPLCONF ACK <offset> 命令
        // 告知主服务器,从服务器已处理的复制流的偏移量
        } else if (!strcasecmp(c->argv[j]->ptr,"ack")) {
            // 从服务器使用 REPLCONF ACK 告知主服务器,
            // 从服务器目前已处理的复制流的偏移量
            long long offset;
            if (!(c->flags & REDIS_SLAVE)) return;
            //将参数中从服务器目前的复制偏移量写入从服务器对应客户端的c->repl_ack_off中
            if ((getLongLongFromObject(c->argv[j+1], &offset) != REDIS_OK))
                return;
            // 如果 offset 已改变,那么更新
            if (offset > c->repl_ack_off)
                c->repl_ack_off = offset;
            // 更新最近一次发送 ack 的时间
            c->repl_ack_time = server.unixtime;
            return;
        //格式错误
        } else {
            addReplyErrorFormat(c,"Unrecognized REPLCONF option: %s",
                (char*)c->argv[j]->ptr);
            return;
        }
    }
    addReply(c,shared.ok);
}

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

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

如果从节点发来的是"REPLCONF ACK <offset>"命令,则从中取出<offset>信息,保存从服务器对应客户端的c->repl_ack_off属性中,告知主服务器从服务器目前已处理的复制流的偏移量,并更新最近一次发送 ack 的时间;

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

2.完全重同步时,从节点状态转换

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

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

从收到从节点的”PSYNC”命令开始,主节点开始为该从节点保存状态。从此时起,站在主节点的角度,从节点的状态会发生转换。主节点为从节点保存的状态记录在客户端结构中的replstate属性中从主节点的角度看,从节点需要经历的状态分别是:REDIS_REPL_WAIT_BGSAVE_START、REDIS_REPL_WAIT_BGSAVE_END、REDIS_REPL_SEND_BULK和REDIS_REPL_ONLINE。

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

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

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

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

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

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

 3.PSYNC命令的处理

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

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

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

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

void syncCommand(redisClient *c) {
    // 如果这是一个从服务器,但其复制状态不是REDIS_REPL_CONNECTED,
    // 说明当前的从节点实例,但还没有到接收并加载完其主节点发来的RDB数据的步骤,
    // 这种情况下,该从节点实例是不能为其下游从节点进行同步的
    if (server.masterhost && server.repl_state != REDIS_REPL_CONNECTED) {
        addReplyError(c,"Can't SYNC while not connected with my master");
        return;
    }
    // 因为主节点接下来需要为该从节点进行后台RDB数据转储
    // 同时需要将前台接收到的其他客户端命令请求缓存到该从节点客户端的输出缓存中,
    // 这就需要一个完全清空的输出缓存,才能为该从节点保存从执行BGSAVE开始的命令流。
    // 因此,如果从节点客户端的输出缓存中尚有数据,直接回复错误信息,不能 SYNC。
    if (listLength(c->reply) != 0 || c->bufpos != 0) {
        addReplyError(c,"SYNC and PSYNC are invalid with pending output");
        return;
    }
    //redisLog(REDIS_NOTICE,"Slave asks for synchronization");
    /* 如果这是一个 PSYNC 命令:
     * (1)那么尝试进行部分重同步PSYNC;
     * (2)如果部分重同步失败,那么检查是否为强制执行完整重同步命令;
     * (3)如果不是强制完整重同步命令,则记录部分重同步失败次数;
     *
     * 情况(1)直接执行部分重同步,情况(2)(3)则继续向后执行完整重同步
     */
    if (!strcasecmp(c->argv[0]->ptr,"psync")) {
        // 尝试进行部分重同步PSYNC
        if (masterTryPartialResynchronization(c) == REDIS_OK) {
            // 部分重同步PSYNC 成功执行的次数增加
            server.stat_sync_partial_ok++;
            return;

        // 不可执行部分重同步PSYNC
        } else {
            //如果master_runid[0] != '?',说明不是强制执行完整重同步,而是执行部分重同步失败
            char *master_runid = c->argv[1]->ptr;
            if (master_runid[0] != '?')

                // 部分重同步PSYNC 失败执行的次数增加
                server.stat_sync_partial_err++;
        }
    } else {
        addReply(c,shared.syntaxerr);
    }
    // 以下是完整重同步的情况。。。
    // 增加执行完整同步的计数
    server.stat_sync_full++;
    // 检查是否有 BGSAVE 在执行
    if (server.rdb_child_pid != -1) {
        redisClient *slave;
        listNode *ln;
        listIter li;
       //遍寻从节点列表
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            slave = ln->value;
            if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break;
        }
        /* 情况1:遍历从服务器列表,如果有至少一个 slave 在等待这个 BGSAVE 完成
         * 那么说明正在进行的 BGSAVE 所产生的 RDB 也可以为其他 slave 所用*/
        if (ln) {
            //在后台进行有硬盘复制的RDB数据转储尚未完成时,如果又有新的从节点B发来了”SYNC”或”PSYNC”命令,同样需要完全重同步。
            //此时主节点后台正在进行RDB数据转储,而且已经为A缓存了命令流。
            //那么从节点B完全可以重用这份RDB数据,而无需再执行一次RDB转储了。
            //而且将A中的输出缓存复制到B的输出缓存中,就能保证B的数据库状态也能与主节点一致了。
            //因此,直接将B的复制状态直接置为REDIS_REPL_WAIT_BGSAVE_END,等到后台RDB数据转储完成时,直接将该转储文件同时发送给从节点A和B即可。
            copyClientOutputBuffer(c,slave);
            c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
            //redisLog(REDIS_NOTICE,"Waiting for end of BGSAVE for SYNC");
        /* 情况2:如果找不到这样的从节点客户端,则主节点需要在当前的BGSAVE操作完成之后,重新执行一次BGSAVE操作*/
        } else {
            c->replstate = REDIS_REPL_WAIT_BGSAVE_START;
            //redisLog(REDIS_NOTICE,"Waiting for next BGSAVE for SYNC");
        }
  /* 情况3:如果当前没有子进程在进行RDB转储,则调用rdbSaveBackground开始进行BGSAVE操作*/
    } else {
        // 没有 BGSAVE 在进行,开始一个新的 BGSAVE
        //redisLog(REDIS_NOTICE,"Starting BGSAVE for SYNC");
        if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) {
            //redisLog(REDIS_NOTICE,"Replication failed, can't BGSAVE");
            addReplyError(c,"Unable to perform background save");
            return;
        }
        // 设置状态
        c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
        // 因为新 slave 进入,刷新复制脚本缓存??????
        //replicationScriptCacheFlush();
    }
    // //如果server.repl_disable_tcp_nodelay选项为真,则取消与从节点通信的socket描述符的TCP_NODELAY选项?????
    // if (server.repl_disable_tcp_nodelay)
    //     anetDisableTcpNoDelay(NULL, c->fd); 

    //初始化从服务器客户端中用于保存主服务器传来的 RDB 文件的文件描述符
    c->repldbfd = -1;
    
    //强制后续将回复缓冲区的命令发送给从服务器时,选择正确的数据库
    server.slaveseldb = -1;
    if (!(c->flags & REDIS_SLAVE)){
    //将REDIS_SLAVE标记记录到从节点客户端的标志位中,以标识该客户端为从节点客户端
    c->flags |= REDIS_SLAVE;
    // 将从节点客户端添加到 slave 列表中
    listAddNodeTail(server.slaves,c);
    }
    // 如果是第一个 slave ,那么初始化 backlog
    if (listLength(server.slaves) == 1 && server.repl_backlog == NULL)
        createReplicationBacklog();
    return;
}

如果当前主节点是其他主节点的从节点,并且该节点复制状态不是REDIS_REPL_CONNECTED,说明当前的从节点实例,还没有到接收并加载完其主节点发来的RDB数据的步骤,这种情况下,该从节点实例是不能为其下游从节点进行同步的,因此向其客户端回复错误信息,然后返回;

如果当前的从节点客户端回复缓存中已经有数据了,说明在PSYNC命令之前的命令交互中,该Redis实例尚有回复信息还没有完全发送给该从节点客户端,这种情况下,向该从节点客户端回复错误信息,然后返回;

这是因为主节点接下来需要为该从节点进行后台RDB数据转储了,同时需要将前台接收到的其他客户端命令请求缓存到该从节点客户端的输出缓存中,这就需要一个完全清空的输出缓存,才能为该从节点保存从执行BGSAVE开始的命令流。因此,如果从节点客户端的输出缓存中尚有数据,直接回复错误信息。

在主节点收到从节点发来的PSYNC命令之前,主从节点之间的交互信息都是比较短的,因此,在网络正常的情况下,从节点客户端中的输出缓存应该是很容易就发送给该从节点,并清空的。

 接下来开始处理PSYNC命令:

如果用户发来的是"PSYNC"命令,则首先调用masterTryPartialResynchronization尝试进行部分重同步,如果成功,则直接返回即可。
如果不能为该从节点执行部分重同步,则接下来需要进行完全重同步了,先增加执行完整同步的计数server.stat_sync_full的值。

接下来开始分情况处理:

情况1:如果当前已有子进程正在后台将RDB转储到本地文件,则轮训列表server.slaves,找到一个复制状态为REDIS_REPL_WAIT_BGSAVE_END的从节点客户端。

如果找到了一个这样的从节点客户端A,并且A的能力是大于当前从节点的。那么主节点为从节点A,在后台开始进行RDB数据转储时,同时会将前台收到的命令流缓存到从节点A的输出缓存中。因此当前发来PSYNC命令的从节点完全可以重用这份RDB数据,以及从节点A中缓存的命令流,而无需再执行一次RDB转储。等到本次BGSAVE完成之后,只需要将RDB文件发送给A以及当前从节点即可。

因此,找到这样的从节点A后,只要复制A的输出缓存中的内容到当前从节点的输出缓存中,然后将该从节点客户端的复制状态置为REDIS_REPL_WAIT_BGSAVE_END即可;

情况2:如果找不到这样的从节点客户端,将该从节点客户端的复制状态置为REDIS_REPL_WAIT_BGSAVE_START,主节点需要在当前的BGSAVE操作完成之后,重新执行一次BGSAVE操作。

情况3:如果当前没有子进程在进行RDB转储,则调用rdbSaveBackground开始进行BGSAVE操作,然后将该从节点客户端的复制状态置为REDIS_REPL_WAIT_BGSAVE_END即可;

处理完上述三种请款以后,将REDIS_SLAVE标记记录到从节点客户端的标志位中,以标识该客户端为从节点客户端,将从节点客户端添加到 server.slaves列表中。

最后,如果当前的列表server.slaves长度为1,并且server.repl_backlog为NULL,说明当前从节点客户端是该主节点的第一个从节点,因此调用createReplicationBacklog创建复制积压缓冲区;
createReplicationBacklog函数代码如下:

void createReplicationBacklog(void) {
    //先验复制积压缓冲区为空
    assert(server.repl_backlog == NULL);
    // 为复制积压缓冲区分配空间
    server.repl_backlog = zmalloc(server.repl_backlog_size);
    // 初始化backlog中数据长度
    server.repl_backlog_histlen = 0;
    // 初始化backlog 的当前索引,增加数据时使用
    server.repl_backlog_idx = 0;
    // 每次创建 backlog 时都将 master_repl_offset 增一
    // 这是为了防止之前使用过 backlog 的从服务器引发错误的 PSYNC 请求
    server.master_repl_offset++;
    // 尽管没有任何数据,
    // 但 backlog 第一个字节的逻辑位置应该是 repl_offset 后的第一个字节
    server.repl_backlog_off = server.master_repl_offset+1;
}

 4.为从节点累积命令流

从主节点在为从节点执行BGSAVE操作的时刻起,准确的说是从节点的复制状态变为REDIS_REPL_WAIT_BGSAVE_END的时刻起,主节点就需要将收到的客户端命令请求,缓存一份到从节点的输出缓存中,也就是为从节点累积命令流。等到从节点状态变为REDIS_REPL_ONLINE时,就可以将累积的命令流发送给从节点了,从而保证了从节点的数据库状态能够与主节点保持一致。

前面提到过,主节点收到”PSYNC”命令后,调用syncCommand时处理时,就需要保证从节点的输出缓存是空的,而且即使是需要回复从节点"+FULLRESYNC"时,也是调用write,将信息直接发送给从节点客户端,而没有使用客户端的输出缓存。这就是因为要使用客户端的输出缓存来为从节点累积命令流。

当主节点收到客户端发来的命令请求后,会调用call函数执行相应的命令处理函数。在call函数中,有下面的语句:

    //根据情况将客户端发送来的命令放入复制状态不是REDIS_REPL_WAIT_BGSAVE_START的从节点客户端回复缓冲区中
    //当从节点的复制状态变为REDIS_REPL_WAIT_BGSAVE_ONLINE时,将回复缓冲区中的内容发送给从节点
    if(listLength(server.slaves))
    {
         replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);
    }

判断当前服务器的从节点个数不为0,调用replicationFeedSlaves函数将命令发送到复制状态不是REDIS_REPL_WAIT_BGSAVE_START的从节点客户端回复缓冲区中。replicationFeedSlaves函数代码如下:

void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
    listNode *ln;
    listIter li;
    int j, len;
    char llstr[REDIS_LONGSTR_SIZE];
    // backlog 为空,且没有从服务器,直接返回
    if (server.repl_backlog == NULL && listLength(slaves) == 0) return;
    // 如果当前命令的数据库id不等于server.slaveseldb,
    // 则需要向积压队列和所有状态不是REDIS_REPL_WAIT_BGSAVE_START的从节点输出缓存中添加一条"SELECT"命令。
    if (server.slaveseldb != dictid) {
        robj *selectcmd;
        //当数据库编号在共享的选择数据库命令范围内,直接调用共享命令
        if (dictid >= 0 && dictid < REDIS_SHARED_SELECT_CMDS) {
            selectcmd = shared.select[dictid];
        } else {
            //否则,创建选择数据库命令
            int dictid_len;
            dictid_len = ll2string(llstr,sizeof(llstr),dictid);
            selectcmd = createObject(REDIS_STRING,
                sdscatprintf(sdsempty(),
                "*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n",
                dictid_len, llstr));
        }
        // 将用于选择数据库的 SELECT 命令添加到 复制积压缓冲区backlog中
        if (server.repl_backlog) feedReplicationBacklogWithObject(selectcmd);
        // 将选择数据库的命令发送给所有从服务器
        listRewind(slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = ln->value;
            // 不要给正在等待 BGSAVE 开始的从服务器发送命令
            if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) continue;
            addReply(slave,selectcmd);
        }
        //释放保存选择数据库的临时命令的对象
        if (dictid < 0 || dictid >= REDIS_SHARED_SELECT_CMDS)
            decrRefCount(selectcmd);
    }
    //更新当前进行主从复制的数据库编号
    server.slaveseldb = dictid;
    //eg:"*3\r\n$3\r\nSET\r\n$4\r\nNAME\r\n$3\r\nOYW\r\n"
    // 将命令写入到复制积压缓冲区backlog中
    if (server.repl_backlog) {
        char aux[REDIS_LONGSTR_SIZE+3];
        //*3\r\n
        aux[0] = '*';
        len = ll2string(aux+1,sizeof(aux)-1,argc);
        aux[len+1] = '\r';
        aux[len+2] = '\n';
        feedReplicationBacklog(aux,len+3);
        //*3\r\n$3\r\nSET\r\n
        //*3\r\n$3\r\nSET\r\n$4\r\nNAME\r\n
        //*3\r\n$3\r\nSET\r\n$4\r\nNAME\r\n$3\r\nOYW\r\n
        for (j = 0; j < argc; j++) {
            long objlen = stringObjectLen(argv[j]);
            // 将参数从对象转换成协议格式
            aux[0] = '$';
            len = ll2string(aux+1,sizeof(aux)-1,objlen);
            aux[len+1] = '\r';
            aux[len+2] = '\n';
            feedReplicationBacklog(aux,len+3);
            feedReplicationBacklogWithObject(argv[j]);
            feedReplicationBacklog(aux+len+1,2);
        }
    }
    //遍历从服务器列表,将命令发送给所有从服务器
    listRewind(slaves,&li);
    while((ln = listNext(&li))) {
        // 指向从服务器
        redisClient *slave = ln->value;
        // 不要给正在等待 BGSAVE 开始的从服务器发送命令
        if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) continue;
        //将命令的参数个数发送给从服务器 *3\r\n
        addReplyMultiBulkLen(slave,argc);
        //将命令的参数发送给从服务器 “$3\r\nSET\r\n”、”$4\r\nNAME\r\n“、”$3\r\nOYW\r\n“
        for (j = 0; j < argc; j++)
            addReplyBulk(slave,argv[j]);
    }
}

该函数用于主节点将收到的客户端命令请求,缓存到积压队列以及所有状态不是REDIS_REPL_WAIT_BGSAVE_START的从节点的输出缓存中。也就是说,当从节点的状态变为REDIS_REPL_WAIT_BGSAVE_END的那一刻起,主节点就一直会为从节点缓存命令流。

这里要注意的是:如果当前命令的数据库id不等于server.slaveseldb的话,就需要向积压队列和所有状态不是REDIS_REPL_WAIT_BGSAVE_START的从节点输出缓存中添加一条"SELECT"命令。这也就是为什么在函数replicationSetupSlaveForFullResync中,将server.slaveseldb置为-1原因了。这样保证第一次调用本函数时,强制增加一条"SELECT"命令到积压队列和从节点输出缓存中。

这里在向从节点的输出缓存中追加命令流时,调用的是addReply类的函数。这些函数用于将信息添加到客户端的输出缓存中,这些函数首先都会调用prepareClientToWrite函数,注册socket描述符上的可写事件,然后将回复信息写入到客户端输出缓存中。

但是在从节点的复制状态变为REDIS_REPL_ONLINE之前,是不能将命令流发送给从节点的。因此,需要在prepareClientToWrite函数中进行特殊处理。在该函数中,有下面的代码:

    // 一般情况,为客户端套接字安装写处理器到事件循环
    // 注意:在从节点的复制状态变为REDIS_REPL_ONLINE之前,是不能将命令流发送给从节点的
    if (c->bufpos == 0 && listLength(c->reply) == 0 &&
        (c->replstate == REDIS_REPL_NONE ||
         c->replstate == REDIS_REPL_ONLINE) &&
        aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
        sendReplyToClient, c) == AE_ERR) return REDIS_ERR;

    return REDIS_OK;
}

上面的代码保证了,当从节点客户端的复制状态尚未真正的变为REDIS_REPL_ONLINE时,是不会注册socket描述符上的可写事件的。

还需要注意的是,在写事件的回调函数sendReplyToClient中,有下面的代码:

    //当客户端回复缓冲区中没有内容,则将该客户写事件从epoll红黑树中移除,
    //并从已注册事件数组中移除
    if (c->bufpos == 0 && listLength(c->reply) == 0) {
        c->sentlen = 0;
        // 删除 write handler
        aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
        // 如果指定了写入之后关闭客户端 FLAG ,那么关闭客户端
        if (c->flags & REDIS_CLOSE_AFTER_REPLY) 
        freeClient(c);
    }

因此,当输出缓存中的内容全部发给客户端之后,就会删除socket描述符上的可写事件。这就保证了在主节点收到SYNC或PSYNC命令后,从节点的输出缓存为空时,该从节点的socket描述符上是没有注册可写事件的。

5.BGSAVE操作完成

当主节点在后台执行BGSAVE的子进程结束之后,主节点父进程wait到该子进程的退出状态后,会调用updateSlavesWaitingBgsave进行BGSAVE的收尾工作。

前面在”PSYNC命令的处理”一节中提到过,如果主节点为从节点在后台进行RDB数据转储时,如果有新的从节点的PSYNC命令到来。则在该新从节点无法复用当前正在转储的RDB数据的情况下,主节点需要在当前BGSAVE操作之后,重新进行一次BGSAVE操作。这就是在updateSlavesWaitingBgsave函数中进行的。updateSlavesWaitingBgsave函数的代码如下:

void updateSlavesWaitingBgsave(int bgsaveerr) {
    listNode *ln;
    int startbgsave = 0;
    listIter li;
    // 遍历列表server.slaves
    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        redisClient *slave = ln->value;
        // 如果从节点客户端当前的复制状态为REDIS_REPL_WAIT_BGSAVE_START,
        // 说明该从节点是在后台子进程进行RDB数据转储期间,连接到主节点上的,并且没有合适的其他从节点可以进行复用。
        if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) {
            // 需要开始新的 BGSAVE,并修改从节点的复制状态为REDIS_REPL_WAIT_BGSAVE_END
            startbgsave = 1;
            slave->replstate = REDIS_REPL_WAIT_BGSAVE_END;
        //当服务器正在进行RDB数据转储,且从节点的复制状态为REDIS_REPL_WAIT_BGSAVE_END时说明该从节点正在等待RDB数据处理完成
        } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {
            /* 执行到这里,说明有 slave 在等待 BGSAVE 完成 */ 
            struct stat buf;
            // 参数bgsaveerr表示后台子进程的退出状态,如果 BGSAVE 执行错误
            if (bgsaveerr != REDIS_OK) {
                // 释放临时 slave,继续遍历下一个从节点
                freeClient(slave);
                //redisLog(REDIS_WARNING,"SYNC failed. BGSAVE child returned an error");
                continue;
            }
            // 当存在从节点复制状态为REDIS_REPL_WAIT_BGSAVE_END且当前BGSAVE成功时
            // 打开 RDB 文件
            if ((slave->repldbfd = open(server.rdb_filename,O_RDONLY)) == -1 ||
                fstat(slave->repldbfd,&buf) == -1) {
                freeClient(slave);
                //redisLog(REDIS_WARNING,"SYNC failed. Can't open/stat DB after BGSAVE: %s", strerror(errno));
                continue;
            }
            /*设置偏移量*/
            //已经向从节点发送的RDB数据的字节数;
            slave->repldboff = 0;
            //获取RDB文件的大小
            slave->repldbsize = buf.st_size;
            // 更新从节点复制状态为REDIS_REPL_SEND_BULK
            slave->replstate = REDIS_REPL_SEND_BULK;
            //更新需要发送给从节点客户端的RDB文件的长度信息
            slave->replpreamble = sdscatprintf(sdsempty(),"$%lld\r\n",
                (unsigned long long) slave->repldbsize);
            // 清空之前的写事件处理器
            aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
            // 将 sendBulkToSlave 安装为 该从节点slave 的写事件处理器
            // 它用于将 RDB 文件发送给 该从节点slave
            if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) {
                freeClient(slave);
                continue;
            }
        }
    }
    // 需要执行新的 BGSAVE
    if (startbgsave) {
        // 开始行的 BGSAVE ,并清空脚本缓存?????
        //replicationScriptCacheFlush();
        //执行后台BGSAVE
        //如果执行后台BGSAVE失败,则释放所有依赖该BGSAVE的从节点客户端
        if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) {
            listIter li;
            listRewind(server.slaves,&li);
            //redisLog(REDIS_WARNING,"SYNC failed. BGSAVE failed");
            while((ln = listNext(&li))) {
                redisClient *slave = ln->value;
                //释放所有复制状态为REDIS_REPL_WAIT_BGSAVE_START的从节点
                if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START)
                    freeClient(slave);
            }
        }
    }
}

参数bgsaveerr表示后台子进程的退出状态;

在函数中,轮训列表server.slaves,针对其中的每一个从节点客户端。如果有从节点客户端当前的复制状态为REDIS_REPL_WAIT_BGSAVE_START,说明该从节点是在后台子进程进行RDB数据转储期间,连接到主节点上的。并且没有合适的其他从节点可以进行复用。这种情况下,需要重新进行RDB数据转储或发送,因此置startbgsave为1,从节点的复制状态设为REDIS_REPL_WAIT_BGSAVE_END

如果从节点客户端当前的状态为REDIS_REPL_WAIT_BGSAVE_END,说明该从节点正在等待RDB数据处理完成(等待RDB转储到文件完成或者等待RDB数据发送完成)。

如果bgsaveerr为REDIS_ERR,说明BGSAVE出错,则直接调用freeClient释放该从节点客户端;

如果bgsaveerr为REDIS_OK,打开RDB文件,描述符记录到slave->repldbfd中;置slave->repldboff为0;置slave->repldbsize为RDB文件大小;置从节点客户端的复制状态为REDIS_REPL_SEND_BULK;置slave->replpreamble为需要发送给从节点客户端的RDB文件的长度信息。从节点通过该信息判断要读取多少字节的RDB数据,这也是为什么有硬盘复制的RDB数据,不需要等待从节点第一个"replconf ack <offset>"命令,而可以直接在发送完RDB数据之后,直接调用putSlaveOnline将该从节点置为REDIS_REPL_ONLINE状态;

然后重新注册从节点客户端的socket描述符上的可写事件,事件回调函数为sendBulkToSlave,它用于将 RDB 文件发送给 该从节点slave;

轮训完所有从节点客户端之后,如果startbgsave为1,则调用函数rdbSaveBackground,重新开始一次BGRDB数据处理过程。

有硬盘复制的RDB数据,接下来需要把RDB文件发送给所有从节点。这是通过从节点socket描述符上的可写事件的回调函数sendBulkToSlave实现的。在该函数中,需要用到从节点客户端的下列属性:

slave->repldbfd,表示打开的RDB文件描述符;
slave->repldbsize,表示RDB文件的大小;
slave->repldboff,表示已经向从节点发送的RDB数据的字节数;
slave->replpreamble,表示需要发送给从节点客户端的RDB文件的长度信息;

 sendBulkToSlave函数的代码如下:

void sendBulkToSlave(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *slave = privdata;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    char buf[REDIS_IOBUF_LEN];
    ssize_t nwritten, buflen;
    //向从节点发送要发送给从节点客户端的RDB文件的长度信息;
    if (slave->replpreamble) {
        nwritten = write(fd,slave->replpreamble,sdslen(slave->replpreamble));
        //写入错误则打印日志,释放用于表示从节点的临时客户端
        if (nwritten == -1) {
            //redisLog(REDIS_VERBOSE,"Write error sending RDB preamble to slave: %s", strerror(errno));
            freeClient(slave);
            return;
        }
        //裁剪slave->replpreamble时发现出现短写,直接退出
        sdsrange(slave->replpreamble,nwritten,-1);
        if (sdslen(slave->replpreamble) == 0) {
            sdsfree(slave->replpreamble);
            slave->replpreamble = NULL;
            /* fall through sending data. */
        } else {
            return;
        }
    }
    //调用lseek将文件指针定位到该文件中未发送的位置,也就是slave->repldboff的位置
    lseek(slave->repldbfd,slave->repldboff,SEEK_SET);
    // 然后调用read,读取RDB文件中REDIS_IOBUF_LEN个字节到buf中;
    buflen = read(slave->repldbfd,buf,REDIS_IOBUF_LEN);
    //检查读取失败则打印日志,释放用于表示从节点的临时客户端
    if (buflen <= 0) {
        //redisLog(REDIS_WARNING,"Read error sending DB to slave: %s", (buflen == 0) ? "premature EOF" : strerror(errno));
        freeClient(slave);
        return;
    }
    // 调用write,将已读取的数据发送给从节点客户端,write返回值为nwritten,将其加到slave->repldboff中。
    if ((nwritten = write(fd,buf,buflen)) == -1) {
        if (errno != EAGAIN) {
            //redisLog(REDIS_WARNING,"Write error sending DB to slave: %s", strerror(errno));
            freeClient(slave);
        }
        return;
    }
    // 如果写入成功,那么更新写入字节数到 repldboff ,等待下次继续写入
    slave->repldboff += nwritten;

    // 如果从节点已经将主服务器传来的RDB文件内容写入完成
    if (slave->repldboff == slave->repldbsize) {
        // 关闭 RDB 文件描述符
        close(slave->repldbfd);
        slave->repldbfd = -1;
        // 删除之前绑定的写事件处理器,将从节点对应的监听写时间从监听句柄中移除
        aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
        // 将状态更新为 REDIS_REPL_ONLINE
        slave->replstate = REDIS_REPL_ONLINE;
        // 更新响应时间
        slave->repl_ack_time = server.unixtime;
        /*接下来就可以开始向该从节点客户端发送累积的命令流*/
        // 创建向从服务器发送累积命令的写事件处理器
        // 将保存并发送 RDB 期间的回复全部发送给从服务器
        if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE,
            sendReplyToClient, slave) == AE_ERR) {
            //redisLog(REDIS_WARNING,"Unable to register writable event for slave bulk transfer: %s", strerror(errno));
            freeClient(slave);
            return;
        }
        // 刷新低延迟 slave 数量
        refreshGoodSlavesCount();
        //redisLog(REDIS_NOTICE,"Synchronization with slave succeeded");
    }
}

如果slave->replpreamble不为NULL,说明需要发送给从节点客户端RDB数据的长度信息,因此,直接调用write向从节点客户端发送slave->replpreamble中的信息。如果写入了部分数据,则将slave->replpreamble更新为未发送的数据,如果slave->replpreamble中的数据已全部发送完成,则释放slave->replpreamble,置其为NULL;否则,直接返回,下次可写事件触发时,接着向从节点发送slave->replpreamble信息;

如果slave->replpreamble为NULL,说明已经发送完长度信息了,接下来就是要发送实际的RDB数据了。

首先调用lseek将文件指针定位到该文件中未发送的位置,也就是slave->repldboff的位置;然后调用read,读取RDB文件中REDIS_IOBUF_LEN个字节到buf中;

然后调用write,将已读取的数据发送给从节点客户端,write返回值为nwritten,将其加到slave->repldboff中。

如果slave->repldboff的值等于slave->repldbsize,则表示RDB文件中的所有数据都发送完成了,因此关闭打开的RDB文件描述符slave->repldbfd;删除socket描述符上的可写事件,然后更改该从节点客户端的复制状态为REDIS_REPL_ONLINE,更新ACK响应时间,接下来就可以开始向该从节点客户端发送累积的命令流了。

注册向从服务器发送累积命令的写事件处理器,事件回调函数为sendReplyToClient,用于向从节点发送缓存的命令流。该函数也是向普通客户端回复命令时的回调函数;

最后,调用refreshGoodSlavesCount,更新当前状态正常的从节点数量;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值