Redis 5.x——主从复制(一)

随着互联网的发展,大多数互联网业务往往读多写少,这时数据库的读会首先成为数据库的瓶颈,如果我们希望能够线性地提升数据库的读写性能,就可以使用读写分离或者主从复制,即主库主要负责增、删、改操作,而从库只负责读操作。当主库有数据修改的时候,从库只需要同步数据即可。可以随着业务的发展增加从库的数量。

Redis数据库读写分离架构图,如图所示。
在这里插入图片描述

1、主从复制概述

主从复制是指将一台Redis服务器的数据复制到其他的Redis服务器,主从是Sentinel(哨兵)和Cluster(集群)模式能够实施的基础。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。默认情况下,每台Redis服务器都是主节点,并且一个主节点可以有零个或多个从节点,但是一个从节点只能有一个主节点。一般主节点负责接收写请求,从节点负责接收读请求,从而实现读写分离。

在Redis复制的基础上,使用与配置主从复制是非常简单的,使得Redis从服务器(slave)能精确地复制Redis主服务器(master)的内容。下面为了行文方便,后文中也会穿插使用master和salve分别指代主服务器或主节点和从服务器或从节点。每次当slave和master之间的连接断开时,slave会自动重连到master上,并且无论这期间master发生了什么,slave都将尝试让自身成为master的精确副本。

这种主从架构的运行依靠以下三个主要机制:

  • 当一个master实例和一个slave实例连接正常时,master会发送一连串的命令流来保持对slave的更新,以便将自身数据集的改变复制给slave(包括客户端的写入、键的过期或被逐出等)​。
  • 当master和slave之间的连接断开之后,因为网络问题或者是master和slave意识到连接超时,slave重新连接上master并会尝试进行部分重新同步,这意味着它会尝试只获取在断开连接期间丢失的命令流(增量同步)​。
  • 当无法进行部分重新同步时,slave会请求进行全量重新同步。这会涉及一个更复杂的过程,比如master需要创建所有数据的快照,再将快照发送给slave,之后在数据集更改时持续发送命令流到slave(增量同步)​。

Redis使用默认的异步复制,其特点是低延迟和高性能,是绝大多数Redis实例的自然复制模式,但是Redis从服务器会异步地确认其从Redis主服务器接收到的数据量。

客户端可以使用WAIT命令来请求同步复制某些特定的数据,但是WAIT命令只能确保在其他Redis实例中有指定数量的已确认副本:在故障转移期间,由于不同原因的故障转移或者根据Redis持久性的实际配置,故障转移期间确认的写入操作可能仍然会丢失。用户可以查看Sentinel(哨兵)模式或Redis集群模式的相关文档,去了解关于高可用性和故障转移的更多信息。

2、主从复制工作原理

2.1、连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备,具体的操作步骤如下:

  1. 保存主节点信息:从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的IP地址和端口号。
  2. 建立socket连接:从节点每秒调用复制定时函数replicationCron一次,如果发现有主节点可以连接,就会根据主节点的IP地址和端口号来建立socket连接。如果连接成功,则从节点为该socket建立一个专门的文件事件处理程序,负责后续的复制工作,如接收RDB文件、接收命令传播等。当主节点接收到从节点的socket连接请求后,为该socket创建相应的客户端状态,并将从节点看作连接到主节点的一个客户端。
  3. 发送ping命令:从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是检查socket连接是否可用,以及主节点当前是否能够处理请求。从节点发送ping命令后,可能会出现下面三种情况:
    • 返回pong:说明socket连接正常,且主节点当前可以处理请求、可以进行复制。
    • 超时:超过一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并尝试重连。
    • 上面两种情况以外的结果:如果主节点返回其他结果,如正在处理超时运行的Lua脚本,说明主节点当前无法处理命令,则从节点断开socket连接并且尝试重连。
  4. 身份验证:如果从节点中设置了masterauth(身份验证)选项,那么从节点需要向主节点进行身份验证;如果没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令,auth命令的参数即为配置文件中的主节点密码的值。如果主节点设置的密码与从节点提供的密码一致,则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接并且尝试重连。
  5. 发送从节点端口信息:身份验证之后,从节点会向主节点发送其监听的端口号,主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中。

开启主从命令slaveof函数的源码如下:

void slaveofCommand(client *c) {
    /* 群集模式下不允许使用SLAVEOF,因为复制是自动进行的 */
    if (server.cluster_enabled) {
        addReplyError(c,"SLAVEOF not allowed in cluster mode.");
        return;
    }

    /* "NO"和 "ONE"组合成特殊的地址和端口号会使当前实例为主节点,
     * 否则会重新设置主节点地址 */
    // 取消主从同步
    if (!strcasecmp(c->argv[1]->ptr,"no")&&
        !strcasecmp(c->argv[2]->ptr,"one")) {
        if (server.masterhost) {
            // 调用取消主从绑定函数,取消主从同步
            replicationUnsetMaster();
            sds client = catClientInfoString(sdsempty(),c);
            serverLog(LL_NOTICE,"MASTER MODE enabled (user request from '%s')",
                client);
            sdsfree(client);
        }
    } else {
        long port;

        if ((getLongFromObjectOrReply(c, c->argv[2],&port, NULL) != C_OK))
            return;
        /* Check if we are already attached to the specified slave */
        // 和主节点建立主从关系并且只能和一个主节点建立连接
        if (server.masterhost&&!strcasecmp(server.masterhost,
c->argv[1]->ptr)
           &&server.masterport == port) {
            serverLog(LL_NOTICE,"SLAVE OF would result into synchronization with
the master we are already connected with. No operation performed.");
            addReplySds(c,sdsnew("+OK Already connected to specified
master\r\n"));
            return;
        }
        /* There was no previous master or the user specified a different one,
         * we can continue. */
        // 设置主节点的IP地址和端口号等信息
        replicationSetMaster(c->argv[1]->ptr, port);
        // 返回当前客户端的信息
        sds client = catClientInfoString(sdsempty(),c);
        serverLog(LL_NOTICE,"SLAVE OF %s:%d enabled (user request from '%s')",
            server.masterhost, server.masterport, client);
        sdsfree(client);
}
// 回复客户端
    addReply(c,shared.ok);
}

主从复制的调度中心replicationCron函数负责监控主从复制过程中的各个状态,并根据不同情况做出不同处理,执行周期为1秒钟执行1次,它的源码如下:

void replicationCron(void) {
    static long long replication_cron_loops = 0;
    if (server.masterhost&&
        (server.repl_state == REPL_STATE_CONNECTING ||
         slaveIsInHandshakeState())&&
         (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
// 超时则取消正在连接从节点的请求
        cancelReplicationHandshake();
    }
      if (server.masterhost&&server.repl_state == REPL_STATE_TRANSFER&&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{   // 超时则取消正在连接从节点的请求
        cancelReplicationHandshake();
    }
    // 从节点连接上主节点(主服务器)后出现交互超时
    if (server.masterhost&&server.repl_state == REPL_STATE_CONNECTED&&
        (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
    {
        freeClient(server.master);
    }
    // 检查是否需要连接主节点
    if (server.repl_state == REPL_STATE_CONNECT) {
        serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
            server.masterhost, server.masterport);
        // 建立与主节点的socket连接
        if (connectWithMaster() == C_OK) {
            serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started");
        }
    }// 从节点发送确定消息给主节点
    if (server.masterhost&&server.master&&
        !(server.master->flags&CLIENT_PRE_PSYNC))
        replicationSendAck();
    listIter li;
    listNode *ln;
    robj *ping_argv[1];
    // 主节点周期性地发送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]);
    }
    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        client *slave = ln->value;
        if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START ||
            (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END&&
             server.rdb_child_type != RDB_CHILD_TYPE_SOCKET))
        {
             // 主节点需要产生一个RDB文件给从节点,并等待RDB文件完成,
             // 但还没发给从节点的时候会发送一个空行
            if (write(slave->fd, "\n", 1) == -1) {
                /* Don't worry, it's just a ping. */
            }
        }
}
    // 如果主节点断开了与从节点的连接
    if (listLength(server.slaves)) {
        listIter li;
        listNode *ln;
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            client *slave = ln->value;
            if (slave->replstate != SLAVE_STATE_ONLINE) continue;
            if (slave->flags&CLIENT_PRE_PSYNC) continue;
            if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
            {
                freeClient(slave);
            }
        }
}
// 如果主节点下面没有从节点就释放掉repl_backlog的内存,因为不需要复制数据
// 给从节点
    if (listLength(server.slaves) == 0&&server.repl_backlog_time_limit&&
        server.repl_backlog)
    {
        time_t idle = server.unixtime - server.repl_no_slaves_since;

        if (idle > server.repl_backlog_time_limit) {
            freeReplicationBacklog();
        }
    }// 如果主节点的AOF持久化功能关闭了而且没有从节点,就释放scriptcache
    if (listLength(server.slaves) == 0&&
        server.aof_state == AOF_OFF&&
        listLength(server.repl_scriptcache_fifo) != 0)
    {
        replicationScriptCacheFlush();
    }// 如果主节点没有在进行持久化操作
    if (server.rdb_child_pid == -1&&server.aof_child_pid == -1) {
        time_t idle, max_idle = 0;
        int slaves_waiting = 0;
        int mincapa = -1;
        listNode *ln;
        listIter li;
        listRewind(server.slaves,&li);
        // 计算等待执行bgsave命令的从节点数量以及最长超时时间和RDB解析能力
        while((ln = listNext(&li))) {
            client *slave = ln->value;
            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) {
                idle = server.unixtime - slave->lastinteraction;
                if (idle > max_idle) max_idle = idle;
                slaves_waiting++;
                mincapa = (mincapa == -1) ? slave->slave_capa :
                                            (mincapa&slave->slave_capa);
            }
        }
        if (slaves_waiting&&max_idle > server.repl_diskless_sync_delay)// 有超时的从节点或者执行bgave命令的从节点就开始执行bgsave命令并复制数据,
         // 默认是5秒
            startBgsaveForReplication(mincapa);
        }
    }
// 刷新本节点的从节点的数量
  refreshGoodSlavesCount();
    replication_cron_loops++; /* Incremented with frequency 1 HZ. */
}

上述代码中从节点的运行过程如下:

  • 从节点每秒运行一次定时任务。
  • 当定时任务发现存在新的主节点后,会调用connectWithMaster函数,尝试与主节点建立网络连接。
  • 建立连接后,由syncWithMaster函数进行后续同步处理。
  • 各种连接超时释放处理。

主节点的运行过程如下:

  • 各种连接超时释放处理。
  • 定期进行PING slave操作。
  • 向从节点写入一个空行,相当于等待ping操作完成。
  • 清理连接超时的所有从节点,如果一个从节点也没有,就直接把backlog(积压缓存内容)释放掉,因为不需要复制数据。
  • 如果主节点未开启磁盘持久化操作且有等待同步的从节点,则会主动启动一个bgsave命令,并提供复制的数据源。

2.2、数据同步阶段

主从节点之间的连接建立好以后便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化,具体执行的方式是:从节点向主节点发送psync命令,并且开始同步。数据同步阶段是主从复制的核心阶段,根据主从节点的当前状态可以分为全量复制和部分复制。

  • 全量复制:当主从连接建立成功后初次复制或无法进行部分复制的时候,将主节点中的所有数据都发送给从节点,比部分复制耗时。
  • 部分复制:主要用于网络中断等情况后的复制,只是将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,就会导致主节点没有能够完整地保存中断期间执行的写命令,这种情况就无法进行部分复制,需要使用全量复制。
2.2.1、全量复制

当启动一个从节点的时候,它会发送一个PSYNC命令给主节点,如果这时从节点重新连接主节点,那么主节点仅仅会复制给从节点部分缺少的数据;如果是从节点第一次连接主节点,就会触发一次全量重新同步(fullresynchronization)。

开始全量重新同步时,主节点会启动一个后台线程,开始生成一份RDB快照文件,同时还会把从客户端收到的所有写命令缓存到内存中。当RDB文件生成完毕之后,主节点会将这个RDB发送给从节点,从节点会先写入本地磁盘,然后从本地磁盘加载到内存中。接着主节点会将内存中缓存的写命令发送给从节点,从节点会同步这些数据。

当从节点判断无法进行部分复制时,会向主节点发送全 量复制的请求,或从节点发送部分复制的请求,当主节点判断无法进行部分复制时也会触发全量复制。除此之外,还有下面几个需要注意的地方:

  1. 主节点收到全量复制的命令后,执行bgsave命令,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令来保证数据的完整性。
  2. 主节点的bgsave命令执行完成后,将RDB文件发送给从节点。从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave命令时的数据库状态。
  3. 主节点将复制缓冲区中的所有写命令并发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态。所以这里会存在一个问题,如果缓冲区有修改的数据,那么从节点收到的数据就不是最新的数据。

全量重新同步的源码如下:

 / replication.c
/* 异步读取从主节点接收的同步数据 */
#define REPL_MAX_WRITTEN_BEFORE_FSYNC (1024*1024*8) /* 8 MB */
void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask){
    char buf[4096];
    ssize_t nread, readlen;
    off_t left;
    UNUSED(el);
    UNUSED(privdata);
    UNUSED(mask);

    /* 用于保存EOF标记和从服务最后接收的字节的静态变量。标记数据是否传输完成 */
    static char eofmark[CONFIG_RUN_ID_SIZE];
    static char lastbytes[CONFIG_RUN_ID_SIZE];
    static int usemark = 0;

    /* 如果 repl_transfer_size == -1 将继续从主节点读取批量的长度 */
    // 先读取数据长度
    if (server.repl_transfer_size == -1) {
        if (syncReadLine(fd,buf,1024,server.repl_syncio_timeout*1000) == -1) {
            serverLog(LL_WARNING,
                "I/O error reading bulk count from MASTER: %s",
                strerror(errno));
            goto error;
        }

        if (buf[0] == '-') {
            serverLog(LL_WARNING,
                "MASTER aborted replication with an error: %s",
                buf+1);
            goto error;
        } else if (buf[0] == '\0') {
            /* 刷新了最后一次ping操作的时间戳 */
            server.repl_transfer_lastio = server.unixtime;
            return;
        } else if (buf[0] != '$') {
            serverLog(LL_WARNING,"Bad protocol from MASTER, the first byte is
not '$' (we received '%s'), are you sure the host and port are right?",
buf);
            goto error;
        }

        /* 批量有效传输有两种形式:一种是$<count>格式,另一种是用于无磁盘传输方式
$EOF:<40 bytes delimiter> */
        if (strncmp(buf+1,"EOF:",4) == 0&&strlen(buf+5) >= CONFIG_RUN_ID_SIZE){
            usemark = 1;
            memcpy(eofmark,buf+5,CONFIG_RUN_ID_SIZE);
            memset(lastbytes,0,CONFIG_RUN_ID_SIZE);
            /* 设置server.repl_transfer_size变量的大小以避免在下次调用时输入
             * 此代码路径 */
            server.repl_transfer_size = 0;
            serverLog(LL_NOTICE,
                "MASTER <-> SLAVE sync: receiving streamed RDB from master");
        } else {
            usemark = 0;
            // 读取数据长度,写入 server.repl_transfer_size,
          // 后续判断是否收到完整数据
            server.repl_transfer_size = strtol(buf+1,NULL,10);
            serverLog(LL_NOTICE,
                "MASTER <-> SLAVE sync: receiving %lld bytes from master",
                (long long) server.repl_transfer_size);
        }
        return;
    }

    /* 批量读取数据 */
    if (usemark) {
        readlen = sizeof(buf);
    } else {
        left = server.repl_transfer_size - server.repl_transfer_read;
        readlen = (left < (signed)sizeof(buf)) ? left : (signed)sizeof(buf);
    }

    nread = read(fd,buf,readlen);
    if (nread <= 0) {
        serverLog(LL_WARNING,"I/O error trying to sync with MASTER: %s",
            (nread == -1) ? strerror(errno) : "connection lost");
        cancelReplicationHandshake();
        return;
    }
    server.stat_net_input_bytes += nread;

    /* 使用标记时,首先检测EOF标记,以避免将EOF标记写入文件 */
    int eof_reached = 0;

    if (usemark) {
        /* Update the last bytes array, and check if it matches our delimiter.*/
        // 更新最后几个字符
        if (nread >= CONFIG_RUN_ID_SIZE) {
            memcpy(lastbytes,buf+nread-CONFIG_RUN_ID_SIZE,
CONFIG_RUN_ID_SIZE);
        } else {
            int rem = CONFIG_RUN_ID_SIZE-nread;
            memmove(lastbytes,lastbytes+nread,rem);
            memcpy(lastbytes+rem,buf,nread);
        }
        if (memcmp(lastbytes,eofmark,CONFIG_RUN_ID_SIZE) == 0) eof_reached = 1;
    }

    server.repl_transfer_lastio = server.unixtime;
    // 将数据写入temp rdb 文件中
    if (write(server.repl_transfer_fd,buf,nread) != nread) {
       serverLog(LL_WARNING,"Write error or short write writing to
the DB dump file needed for MASTER <-> SLAVE synchronization:
 %s", strerror(errno));
        goto error;
    }
    server.repl_transfer_read += nread;

    /* 如果到达EOF标记,则从文件中删除最后40字节 */
    if (usemark&&eof_reached) {
        if (ftruncate(server.repl_transfer_fd,
            server.repl_transfer_read - CONFIG_RUN_ID_SIZE) == -1)
        {
            serverLog(LL_WARNING,"Error truncating the RDB file received from
the master for SYNC: %s", strerror(errno));
            goto error;
        }
    }

    /* 不时的在磁盘上同步数据,否则在传输结束时,由于内存缓冲区被复制到实际磁盘中,
* 这样可能会遭受很大的延迟 */
    // 缓冲达到一定值后,直接刷盘
    // REPL_MAX_WRITTEN_BEFORE_FSYNC: 8M
    if (server.repl_transfer_read >=
        server.repl_transfer_last_fsync_off + REPL_MAX_WRITTEN_BEFORE_FSYNC)
    {
        off_t sync_size = server.repl_transfer_read -
                          server.repl_transfer_last_fsync_off;
        rdb_fsync_range(server.repl_transfer_fd,
            server.repl_transfer_last_fsync_off, sync_size);
        server.repl_transfer_last_fsync_off += sync_size;
    }

    /* Check if the transfer is now complete */
    // 传输完成
    if (!usemark) {
        if (server.repl_transfer_read == server.repl_transfer_size)
            eof_reached = 1;
    }

    if (eof_reached) {
        // 直接将临时RDB文件改名为正式的RDB文件,从而实现数据替换
        if (rename(server.repl_transfer_tmpfile,server.rdb_filename) == -1) {
            serverLog(LL_WARNING,"Failed trying to rename the temp DB into
dump.rdb in MASTER <-> SLAVE synchronization: %s", strerror(errno));
            cancelReplicationHandshake();
            return;
        }
        serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Flushing old data");
        // 清空原来的数据,刷入新数据
        signalFlushedDb(-1);
        emptyDb(
            -1,
            server.repl_slave_lazy_flush ? EMPTYDB_ASYNC : EMPTYDB_NO_FLAGS,
            replicationEmptyDbCallback);
        /* 因为rdbLoad()函数将调用事件循环来不时的处理事件以进行非阻塞加载。
* 所以,在将DB加载到内存中之前,我们需要删除可读的处理程序 */
        aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);
        serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory");
        // 重新载入RDB文件,从而完成同步操作
        if (rdbLoad(server.rdb_filename) != C_OK) {
            serverLog(LL_WARNING,"Failed trying to load the MASTER
synchronization DB from disk");
            cancelReplicationHandshake();
            return;
        }
        /* 连接的从链路最终设置为主链路 */
        zfree(server.repl_transfer_tmpfile);
        close(server.repl_transfer_fd);
        // 设置主节点信息,以便下次直接使用
        replicationCreateMasterClient(server.repl_transfer_s);
        serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
        /* 现在已经完成同步,将重新启动AOF子系统。而这将触发AOF重写机制,
* 完成后将开始附加到新文件 */
        if (server.aof_state != AOF_OFF) {
            int retry = 10;
            // 重新关联AOF文件,以便后续可以正常写入AOF文件
            stopAppendOnly();
            while (retry--&&startAppendOnly() == C_ERR) {
                serverLog(LL_WARNING,"Failed enabling the AOF after successful
master synchronization! Trying it again in one second.");
                sleep(1);
            }
            if (!retry) {
                serverLog(LL_WARNING,"FATAL: this slave instance finished the
synchronization with its master, but the AOF can't be turned on.
Exiting now.");
                exit(1);
            }
        }
    }

    return;
error:
    cancelReplicationHandshake();
    return;
}

上述代码就是全量复制的整体过程,主要分为下面几个部分:

  1. 读取整体数据的长度。
  2. 依次读取可以进行复制的数据。
  3. 达到一定缓冲区数量后强制进行刷盘。
  4. 主节点传输完成后,从节点切换最新的RDB文件。
  5. 从节点清空原来的db数据,刷新最新数据。
  6. 设置主节点信息,以便下次直接使用。
2.2.2、部分复制

从全量复制的过程可以看出,全量复制有如下几个非常重要的操作:

  1. 主节点通过bgsave命令派生或创建(fork)子进程进行RDB持久化,这个过程非常消耗CPU、内存以及硬盘IO的资源。
  2. 主节点通过网络将全量的RDB文件发送给从节点,会大量消耗主从节点的带宽流量。
  3. 为了保证数据的一致性,从节点清空旧数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof命令,就会带来额外的消耗。

从上面几个问题我们可以看出全量复制在主节点数据量较大时效率太低,所以从Redis 2.8开始就支持主从复制的断点续传,如果主从复制过程中网络连接断掉了,那么可以接着上次复制的地方继续复制下去,而不是从头开始复制,这样在很大程度上保证了主从复制的性能。部分复制的实现主要依赖于三个重要的特征:

1. 复制偏移量

主节点和从节点分别维护一个复制偏移量(offset),表示主节点向从节点传送的字节数;主节点每次向从节点传播N字节数据时,主节点的偏移量增加N;从节点每次收到主节点传来的N字节数据时,从节点的偏移量增加N。

偏移量用于判断主从节点的数据库状态是否一致:如果主从节点的偏移量相同,则数据一致;如果偏移量不同,则数据不一致,此时可以根据主从节点的偏移量找出从节点缺少的那部分数据。比如,主节点的偏移量是800、从节点的偏移量是600,那么部分复制就需要将偏移量为601到800的数据传送给从节点,而偏移量为601到800的数据存储在复制积压缓冲区(backlog)。

2. 复制积压缓冲区

复制积压缓冲区是专门由主节点维护、固定长度、先进先出的队列,默认大小是1MB。当主节点开始有从节点时才会创建,否则不会创建。它的作用是备份主节点最近发送给从节点的数据。无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区,不会存在多个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区作为写命令的备份,除了存储写命令,复制积压缓冲区中还存储了其中每个字节对应的复制偏移量。因为复制积压缓冲区是定长的且是先进先出的,所以它保存的是主节点最近执行的写命令,时间较早的写命令会被挤出缓冲区。

需要注意的是,该缓冲区的长度固定且有限,因此可以备份的写命令也有限,当主从节点偏移量的差距过大超过缓冲区长度时将无法执行部分复制,只能执行全量复制。所以,为了提高网络中断时部分复制执行的概率,可以根据需要修改配置repl-backlog-size来增大复制积压缓冲区的大小。比如,网络中断的平均时间是60秒,而主节点平均每秒产生的写命令所占的字节数为200KB,则复制积压缓冲区的平均大小为12MB,那么可以设置为24MB来保证绝大多数网络断线情况都可以使用部分复制。

从节点将偏移量发送给主节点后,主节点根据偏移量和缓冲区大小决定能否执行部分复制。如果偏移量之后的数据仍然都在复制积压缓冲区里,则执行部分复制。如果偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出)​,则执行全量复制。所以,复制积压缓冲区的大小是非常重要的一个参数。

3. 服务器运行ID

可以通过info server命令查询到服务运行ID,如图所示。

在这里插入图片描述
启动时每个Redis节点(无论主从节点)都会自动生成一个随机ID(每次启动都不一样,由40个随机的十六进制字符组成)​。runid用来唯一识别一个Redis节点,可以通过info server命令查看。主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点,主节点根据runid判断能否进行部分复制。

如果从节点保存的runid与主节点现在的runid相同,就说明主从节点之前同步过,主节点会继续尝试使用部分复制。如果从节点保存的runid与主节点现在的runid不同,就说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。下面是部分同步的源码:

// replication.c使用cacheMaster执行PSYNC(部分同步)来复制数据
/* 使用作为参数传递的文件描述符作为新主机的套接字,将缓存的主节点转换为当前的主节点。
* 当成功设置部分重新同步时,将调用此函数,以便在主节点离开时,将从缓存主节点接收数据 */
void replicationResurrectCachedMaster(int newfd) {
    server.master = server.cached_master;
    server.cached_master = NULL;
    server.master->fd = newfd;
    server.master->flags&=(CLIENT_CLOSE_AFTER_REPLY|CLIENT_CLOSE_ASAP);
    server.master->authenticated = 1;
    server.master->lastinteraction = server.unixtime;
    server.repl_state = REPL_STATE_CONNECTED;

    /* Re-add to the list of clients. */
    listAddNodeTail(server.clients,server.master);
    // 添加文件epoll事件,由readQueryFromClient函数进行事件处理
    if (aeCreateFileEvent(server.el, newfd, AE_READABLE,
                          readQueryFromClient, server.master)) {
        serverLog(LL_WARNING,"Error resurrecting the cached master, impossible
to add the readable handler: %s", strerror(errno));
        freeClientAsync(server.master); /* Close ASAP. */
    }

    /* 如果写入缓冲区中存在挂起的数据,我们可能还需要设置写入处理程序 */
    // 如果需要发送数据,就建立一个写操作的fileEvent事件
    if (clientHasPendingReplies(server.master)) {
        if (aeCreateFileEvent(server.el, newfd, AE_WRITABLE,
                          sendReplyToClient, server.master)) {
            serverLog(LL_WARNING,"Error resurrecting the cached master,
impossible to add the writable handler: %s", strerror(errno));
            freeClientAsync(server.master); /* Close ASAP. */
        }
    }
}
// readQueryFromClient函数实现部分复制
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask){
    client *c = (client*) privdata;
    int nread, readlen;
    size_t qblen;
    UNUSED(el);
    UNUSED(mask);
    // PROTO_IOBUF_LEN: 1024*16
    // PROTO_MBULK_BIG_ARG: 1024*32
    readlen = PROTO_IOBUF_LEN;
    /* 如果是一个多批量的请求,并且程序正在处理一个足够大的批量请求,则尝试从缓存区
* 请求数据 */
    if (c->reqtype == PROTO_REQ_MULTIBULK&&c->multibulklen&&c->bulklen !=
-1
       &&c->bulklen >= PROTO_MBULK_BIG_ARG)
    {
        int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);

        if (remaining < readlen) readlen = remaining;
    }

    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    // 读取请求的命令
    nread = read(fd, c->querybuf+qblen, readlen);
    if (nread == -1) {
        if (errno == EAGAIN) {
            return;
        } else {
            serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    } else if (nread == 0) {
        serverLog(LL_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }

    sdsIncrLen(c->querybuf,nread);
    c->lastinteraction = server.unixtime;
    if (c->flags&CLIENT_MASTER) c->reploff += nread;
    server.stat_net_input_bytes += nread;
    // 超出最大限制,不处理
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);
        serverLog(LL_WARNING,"Closing client that reached max query buffer
length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }
    // 处理查询数据
    processInputBuffer(c);
}

如上处理主节点部分同步过来的命令只要重新在从节点执行一次即可,而且是基于epoll的事件监听,可以做到持续处理同步数据。

需要注意同步函数的实现,因为这决定了是使用全量复制还是部分复制。slaveTryPartialResynchronization函数的源码如下:

// 尝试进行部分同步
// replication.c
int slaveTryPartialResynchronization(int fd, int read_reply) {
    char *psync_runid;
    char psync_offset[32];
    sds reply;

    /* Writing half */
    // 向主节点写入服务端id和偏移量,每次都拉取一部分数据
    if (!read_reply) {
        /* 初始时将repl_master_initial_offset设置为-1,以将当前主节点的运行id
* 和偏移标记为无效。如果能够使用PSYNC命令进行完全同步,再次会将偏移量设置为
* 正确的值,以便将此信息传播到表示主节点的客户机server.master中 */
        server.repl_master_initial_offset = -1;
        // 如果已经建立了连接,则 psync_runid、psync_offset 都是可以查询到相关信息的
         if (server.cached_master) {
            psync_runid = server.cached_master->replrunid;
            snprintf(psync_offset,sizeof(psync_offset),"%lld",
server.cached_master->reploff+1);
            serverLog(LL_NOTICE,"Trying a partial resynchronization
(request %s:%s).", psync_runid, psync_offset);
        }
// 否则 psync_runid = "?", psync_offset="-1";
else {
            serverLog(LL_NOTICE,"Partial resynchronization not possible (no
cached master)");
            psync_runid = "?";
            memcpy(psync_offset,"-1",3);
        }

        /* Issue the PSYNC command */
        // 首次发送PSYNC命令,需要全量复制
        // 之后的操作发送runid和offset 进行部分复制
        reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_runid,
psync_offset,NULL);
        if (reply != NULL) {
            serverLog(LL_WARNING,"Unable to send PSYNC to master: %s",reply);
            sdsfree(reply);
            aeDeleteFileEvent(server.el,fd,AE_READABLE);
            return PSYNC_WRITE_ERROR;
        }
        return PSYNC_WAIT_REPLY;
    }

    /* Reading half */
    // 读取PSYNC的结果
    reply = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
    if (sdslen(reply) == 0) {
        /*主机可以在收到PSYNC之后和回复之前发送空的换行符,只是为了保持连接的健康状态*/
        sdsfree(reply);
        return PSYNC_WAIT_REPLY;
    }

    aeDeleteFileEvent(server.el,fd,AE_READABLE);
    // +FULLRESYNC 代表需要进行全量复制,否则进行部分复制
    if (!strncmp(reply,"+FULLRESYNC",11)) {
        char *runid = NULL, *offset = NULL;

        /* FULL RESYNC, parse the reply in order to extract the run id
         * and the replication offset. */
        runid = strchr(reply,' ');
        if (runid) {
            runid++;
            offset = strchr(runid,' ');
            if (offset) offset++;
        }
        // runid 长度为 40
        if (!runid || !offset || (offset-runid-1) != CONFIG_RUN_ID_SIZE) {
            serverLog(LL_WARNING,
                "Master replied with wrong +FULLRESYNC syntax.");
            /* 这是一个意外情况,+FULLRESYNC命令的回复实际上意味着主机支持PSYNC命令,
* 但回复格式似乎错了。为了安全起见,清空主节点运行id,以确保下一个PSYNC
* 命令运行失败 */
            memset(server.repl_master_runid,0,CONFIG_RUN_ID_SIZE+1);
        } else {
            memcpy(server.repl_master_runid, runid, offset-runid-1);
            server.repl_master_runid[CONFIG_RUN_ID_SIZE] = '\0';
            server.repl_master_initial_offset = strtoll(offset,NULL,10);
            serverLog(LL_NOTICE,"Full resync from master: %s:%lld",
                server.repl_master_runid,
                server.repl_master_initial_offset);
        }
        /* 进行完全重新同步,丢弃缓存的主结构 */
        // 全量同步,重置主节点的缓存
        replicationDiscardCachedMaster();
        sdsfree(reply);
        return PSYNC_FULLRESYNC;
    }
    // 部分复制的情况下只会返回 +CONTINUE
    if (!strncmp(reply,"+CONTINUE",9)) {
        /* 已接受部分重新同步,请相应地设置复制状态 */
        serverLog(LL_NOTICE,
            "Successful partial resynchronization with master.");
        // 立即将结果释放
        sdsfree(reply);
        // 执行部分同步方法
        replicationResurrectCachedMaster(fd);
        // 继续使用部分同步
        return PSYNC_CONTINUE;
    }

    /* 如果达到这一点,我们要么收到一个错误,因为主机不能解析PSYNC命令,要么收到来自主机
* 的意外回复。在这两种情况下,都向调用者返回不支持PSYNC的信息 */
    // PSYNC不支持,因处理为降级版本
    if (strncmp(reply,"-ERR",4)) {
        /* If it's not an error, log the unexpected event. */
        serverLog(LL_WARNING,
            "Unexpected reply to PSYNC from master: %s", reply);
    } else {
        serverLog(LL_NOTICE,
            "Master does not support PSYNC or is in "
            "error state (reply: %s)", reply);
    }
    sdsfree(reply);
    replicationDiscardCachedMaster();
return PSYNC_NOT_SUPPORTED;
}

通过上面的过程,我们可以看出该函数尝试获取主节点运行ID以及复制偏移量,并向主节点发送PSYNC命令请求,读取并解析PSYNC命令回复,判断执行全量重新同步还是部分重新同步。在与主节点同步时,主要依赖于PSYNC的返回值决定是全量同步还是部分同步。下图所示为主从复制数据加载的流程图。

在这里插入图片描述

2.2.3、PSYNC命令实现原理

从上面的流程和相应源码可以看出,PSYNC是整个主从复制过程的重要步骤。PSYNC的实现过程如下:

// 用法: PSYNC run_id offset
// replication.c
/* SYNC和PSYNC命令的实现 */
void syncCommand(client *c) {
    /* 如果从机处于监视模式,则忽略同步 */
    // SYNC命令只能被调用一次
    if (c->flags&CLIENT_SLAVE) return;

    /* 如果当前是从机,但与主机的链接不正常,则拒绝同步请求 */
    if (server.masterhost&&server.repl_state != REPL_STATE_CONNECTED) {
        addReplyError(c,"Can't SYNC while not connected with my master");
        return;
    }

    // 输出未完成时不能再进行处理
    if (clientHasPendingReplies(c)) {
        addReplyError(c,"SYNC and PSYNC are invalid with pending output");
        return;
    }

    serverLog(LL_NOTICE,"Slave %s asks for synchronization",
        replicationGetSlaveName(c));
        // psync 将会优先尝试部分复制
    if (!strcasecmp(c->argv[0]->ptr,"psync")) {
        // 部分复制将不会重置标志(flag),每次psync都会成功运行
        if (masterTryPartialResynchronization(c) == C_OK) {
            server.stat_sync_partial_ok++;
            return; /* No full resync needed, return. */
        } else {
            char *master_runid = c->argv[1]->ptr;

            /* 为失败的PSYNC增加统计信息,但仅当运行id不是“?”时,因为当从机无法部
* 分重新同步时,它会被从机用来强制执行完全重新同步 */
            if (master_runid[0] != '?') server.stat_sync_partial_err++;
        }
    } else {

        c->flags |= CLIENT_PRE_PSYNC;
    }
    // 下面是全量复制
    /* Full resynchronization. */
    server.stat_sync_full++;

    /* 将从节点设置为等待BGSAVE启动的节点。如果我们以不同的方式处理从节点,以下代码路径
*  将改变状态 */
    c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
    if (server.repl_disable_tcp_nodelay)
        anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */
    c->repldbfd = -1;
    // 在把从节点添加到主节点的从节点集合中设置SLAVE标识,表示已执行过SYNC操作
    c->flags |= CLIENT_SLAVE;
    listAddNodeTail(server.slaves,c);

    /* CASE 1: BGSAVE is in progress, with disk target. */
    // 如果RDB存储已在进行中,则表示bgsave命令已经在运行
    // 当有后面进来的主从客户端时,只需要通知正在运行bgsave命令
    if (server.rdb_child_pid != -1&&
        server.rdb_child_type == RDB_CHILD_TYPE_DISK)
    {
        /* 检查是否适合复制,是否有另一个从服务器记录了自服务器分叉保存以来的差异 */
        client *slave;
        listNode *ln;
        listIter li;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            slave = ln->value;
            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END) break;
        }
        /* 要连接此从节点,检查它是否具有触发当前BGSAVE的从节点的所有功能 */
        if (ln&&((c->slave_capa&slave->slave_capa) == slave->slave_capa)){
            /* 服务器已经在为另一个从属服务器注册差异。设置正确的状态,并复制缓冲区 */
            copyClientOutputBuffer(c,slave);
            replicationSetupSlaveForFullResync(c,
slave->psync_initial_offset);
            serverLog(LL_NOTICE,"Waiting for end of BGSAVE for SYNC");
        } else {
            /* 不能正常进行,我们需要等待下一次BGSAVE以注册差异 */
            serverLog(LL_NOTICE,"Waiting for next BGSAVE for SYNC");
        }

    /* CASE 2: BGSAVE正在进行,并且具有嵌套字 */
    } else if (server.rdb_child_pid != -1&&
               server.rdb_child_type == RDB_CHILD_TYPE_SOCKET)
    {
        /* 有一个RDB子进程,但它直接写入子套接字。我们需要等待下一次BGSAVE以进行同步 */
        serverLog(LL_NOTICE,"Waiting for next BGSAVE for SYNC");

    /* CASE 3: 不进行BGSAVE */
    } else {
        // 在主节点非持久化方式下不启动bgsave策略
        if (server.repl_diskless_sync&&(c->slave_capa&SLAVE_CAPA_EOF)) {
            /* 无盘复制RDB子进程是在replicationCron()内创建的,因为我们希望将其启动
* 延迟几秒钟,以等待更多从节点到达 */
            if (server.repl_diskless_sync_delay)
                serverLog(LL_NOTICE,"Delay next BGSAVE for SYNC");
        } else {
            /* 没有正在进行的BGSAVE,开启一个磁盘化的BGSAVE*/
            // 主动开启一个后台执行的bgsave命令
            if (startBgsaveForReplication(c->slave_capa) != C_OK) return;
        }
    }
    // 如果是第一个从节点就创建积压缓存区(backlog)
    if (listLength(server.slaves) == 1&&server.repl_backlog == NULL)
        createReplicationBacklog();
        return;
}

// 如果开启了RDB策略,就触发后台执行bgsave命令
// replication.c
int startBgsaveForReplication(int mincapa) {
    int retval;
    int socket_target = server.repl_diskless_sync&&(mincapa&
SLAVE_CAPA_EOF);
    listIter li;
    listNode *ln;

    serverLog(LL_NOTICE,"Starting BGSAVE for SYNC with target: %s",
        socket_target ? "slaves sockets" : "disk");

    if (socket_target)
        // 直接向socket中写入数据同步
        retval = rdbSaveToSlavesSockets();
    else
        // 存储到磁盘RDB文件中
        retval = rdbSaveBackground(server.rdb_filename);

    /* 如果未能正常执行BGSAVE,将从salves列表中删除等待完全重新同步的从节点服务器,
* 向其告知发生的错误,尽快关闭连接 */
    if (retval == C_ERR) {
        serverLog(LL_WARNING,"BGSAVE for replication failed");
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            client *slave = ln->value;

            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) {
                slave->flags&= ~CLIENT_SLAVE;
                listDelNode(server.slaves,ln);
                addReplyError(slave,
                    "BGSAVE failed, replication can't continue");
                slave->flags |= CLIENT_CLOSE_AFTER_REPLY;
            }
        }
        return retval;
    }

    /* 如果目标是socket, rdbSaveToSlavesSockets()已经为完全重新同步设置了salves。
* 否则,对于磁盘目标,请立即执行此操作*/
    if (!socket_target) {
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            client *slave = ln->value;
            // 依次响应从节点和主节点的runid和offset
            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) {
                    replicationSetupSlaveForFullResync(slave,
                            getPsyncInitialOffset());
            }
        }
    }
  ......
}

如上源码体现了如图所示的主从复制过程。
在这里插入图片描述

// replication.c,针对第一个进行主从复制的从节点,需要触发backlog的初始化
void createReplicationBacklog(void) {
    serverAssert(server.repl_backlog == NULL);
    server.repl_backlog = zmalloc(server.repl_backlog_size);
    server.repl_backlog_histlen = 0;
    server.repl_backlog_idx = 0;
    server.master_repl_offset++;
    server.repl_backlog_off = server.master_repl_offset+1;
}

// 部分复制时的处理方式
// replication.c,尝试部分复制
int masterTryPartialResynchronization(client *c) {
    long long psync_offset, psync_len;
    char *master_runid = c->argv[1]->ptr;
    char buf[128];
    int buflen;

    /* 此主服务器的运行id是否与想要的从服务器PSYNC的相同?如果运行id更改了,
* 则表示此主节点是另一个实例,复制操作将无法继续 */
    // run_id 发生了变化,则需要重新同步
    if (strcasecmp(master_runid, server.runid)) {
        /* 要强制完全重新同步的从节点服务器使用运行id */
        if (master_runid[0] != '?') {
            serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
                "Runid mismatch (Client asked for runid '%s', my runid is '%s')",
                master_runid, server.runid);
        } else {
            serverLog(LL_NOTICE,"Full resync requested by slave %s",
                replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }

    /*  判断是否还有从节点需要同步数据 */
    if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
       C_OK) goto need_full_resync;
    // 偏移量超出范围,使用全量同步
    if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off +
server.repl_backlog_histlen))
    {
        serverLog(LL_NOTICE,
            "Unable to partial resync with slave %s for lack of backlog (Slave
request was: %lld).", replicationGetSlaveName(c), psync_offset);
        if (psync_offset > server.master_repl_offset) {
            serverLog(LL_WARNING,
                "Warning: slave %s tried to PSYNC with an offset that is greater
than the master replication offset.",
replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }

    /* 如果满足这一点,我们可以执行部分重新同步:
1)设置客户机状态,使其成为从机
2)通知客户继续
3)将积压数据(从偏移量到末尾)发送到从节点 */
    c->flags |= CLIENT_SLAVE;
    c->replstate = SLAVE_STATE_ONLINE;
    c->repl_ack_time = server.unixtime;
    c->repl_put_online_on_ack = 0;
    listAddNodeTail(server.slaves,c);
    /* 不能使用连接缓冲区,因为在这个阶段它们被用来积累新的命令。但是可以确信套接字发送
* 缓冲区是空的,所以这个写操作实际上永远不会失败 */
    // 响应客户端 +CONTINUE
    buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
    if (write(c->fd,buf,buflen) != buflen) {
        freeClientAsync(c);
        return C_OK;
    }
    // 输出部分同步的数据
    psync_len = addReplyReplicationBacklog(c,psync_offset);
    serverLog(LL_NOTICE,
        "Partial resynchronization request from %s accepted. Sending %lld bytes
of backlog starting from offset %lld.",
            replicationGetSlaveName(c),
            psync_len, psync_offset);
    /* 注意,我们不需要将server.slaveseldb上的selected DB设置为-1来强制主节点
* 发出SELECT,因为从节点在与主节点的上一次连接中已经具有这种状态 */
  // 更新有效从节点的数量
    refreshGoodSlavesCount();
      return C_OK; /* The caller can return, no full resync needed. */
need_full_resync:
   /* 如果需要完全同步,现在不能回复PSYNC。答复必须包含用于传输RDB文件时的主偏移量,因此需要将答复延迟到该时刻。 */
    return C_ERR;
}

通过如上代码可知,即执行部分重新同步需要满足下面的两个条件:

  • 服务器runid与复制偏移量必须合法。
  • 复制偏移量必须包含在复制缓冲区中。

当可以执行部分重新同步时,主节点(即主服务器)便将该客户端添加到自己的从节点(即从服务器)链表slaves中,并把客户端状态标记为SLAVE_STATE_ONLINE。所有流程执行完毕后,主节点每次接收到写命令请求时都会将该命令请求广播给所有从节点,同时记录在复制缓冲区中。广播命令请求的实现函数为replicationFeedSlaves:

 void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc){
  /* 如果与上次选择的数据库不相等,就需要先同步select命令*/
  if (server.slaveseldb != dictid) {
    robj *selectcmd;
    /* 将select命令添加到复制缓冲区 */
    if (server.repl_backlog) feedReplicationBacklogWithObject(selectcmd);
    /* 向所有从节点发送select命令 */
    listRewind(slaves,&li);
    while((ln = listNext(&li))) {
      addReply(slave,selectcmd);
    }
  }
  server.slaveseldb = dictid;
  /* 如果有新命令,将命令写入复制待办事项列表 */
  if (server.repl_backlog) {
  // 将当前命令请求添加到复制缓冲区
    char aux[LONG_STR_SIZE+3];
    /* Add the multi bulk reply length. */
    aux[0] = '*';
    len = ll2string(aux+1,sizeof(aux)-1,argc);
    aux[len+1] = '\r';
    aux[len+2] = '\n';
    feedReplicationBacklog(aux,len+3);
    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))) {
    client *slave = ln->value;
    /* Add the multi bulk length. */
    addReplyArrayLen(slave,argc);
    for (j = 0; j < argc; j++)
    addReplyBulk(slave,argv[j]);
  }
}

从上述源码可知:当前客户端连接的数据库可能并不是上次向从节点同步数据的数据库,因此可能需要先向从节点同步select命令来修改数据库。针对每个写命令,主节点都需要将命令请求同步给所有从节点,同时向从节点同步的每个命令请求都会记录到复制缓冲区中。

2.3、命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段。在这个阶段,主节点将自己执行的写命令发送给从节点,从节点接收到命令并且执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:ping和replconf ack。心跳机制是指主从复制的超时判断,即每隔指定的时间主节点会向从节点发送ping命令,这个ping命令的目的主要是让从节点进行超时判断。心跳机制是在replicationCron函数中实现的,源码如下:

 /* First, send PING according to ping_slave_period. */
    // 发送ping 请求
    // 默认repl_ping_slave_period: 10
    // 默认配置repl_ping_slave_period 是10秒
    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];
    }

ping命令的作用如下:

  1. 虽然主从节点成功建立起了套接字连接,但是双方并未使用该套接字进行过任何通信,通过发送ping命令可以检查套接字的读写状态。
  2. 复制工作接下来的几个步骤都需要在主节点可以正常处理命令请求的状态下进行,通过ping可以知道主节点是否能正常处理命令请求。
  3. 如果主节点返回给从节点一个命令回复,但是从节点不能在规定的时限内读取出命令回复的内容,那么表示主从节点之间的网络连接状态不是很好,不能继续执行复制工作的后续步骤。当出现这种情况时,从节点断开并重新创建连接主节点的套接字。
  4. 如果主节点返回给从节点一个error,则表示主节点暂时没有办法处理从节点的命令请求,不能继续执行复制工作的其余步骤。当出现这种情况时,从节点断开并重新创建和主服务器连接的套接字。
  5. 如果从节点读取到了pong回复,那么表示主从节点之间的网络连接状态正常,并且主节点可以正常处理从节点的请求。在这种情况下,从节点可以继续执行复制工作。

ping出现的各种情况及其处理的流程图,如图所示(图中的主从服务器即主从节点)​。

在这里插入图片描述

在命令传播阶段,从节点会向主节点发送replconf ack命令,频率也是每秒1次;命令格式为:replconfack{offset},其中offset是指从节点保存的复制偏移量。replconf ack命令的意义和作用如下:

  1. 实时监测主从节点的网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中执行infoReplication命令,可以看到其从节点的状态中的lag值(包含从节点的IP地址和端口号以及数据同步偏移量)​,代表的是主节点上次收到该replconf ack命令的时间间隔,在正常情况下,该值应该是0或1。
  2. 检测命令丢失:从节点发送了自身的偏移量,主节点会与自己的偏移量对比,如果从节点因为网络丢包等原因造成数据缺失,主节点会推送缺失的数据。
  3. 辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数来保证主节点在不安全的情况下不会执行写命令。所谓不安全,是指从节点数量太少或延迟过高。

2.4、身份验证

从节点收到主节点返回的“pong”回复之后,下一步要做的就是决定是否进行身份验证:

  • 如果从节点设置了masterauth选项,那么需要进行身份验证。
  • 如果从节点没有设置masterauth选项,那么不进行身份验证。

在需要进行身份验证的情况下,从节点向主节点发送一条auth命令,命令参数为从节点masterauth选项的值。比如,从节点的masterauth选项值为123456,那么从节点会向主节点发送命令auth 123456。

从节点在身份验证阶段可能遇到下面几种情况:

  1. 如果主节点没有设置requirepass选项,并且从节点也没有设置masterauth选项,那么主节点将继续执行从节点发送的命令,复制工作可以继续进行。
  2. 如果从节点通过auth命令发送的密码和主节点设置的requirepass选项相同,那么主节点将继续执行从节点发送的命令,复制工作可以继续进行;如果主从节点设置的密码不相同,则主节点返回一条“invalidpassword”​(密码不正确)的错误提示信息。
  3. 如果主节点设置了requirepass选项,从节点却没有设置masterauth选项,那么主节点将返回一个noauth错误。另外,如果主节点没有设置requirepass选项,但是从节点设置了masterauth选项,那么主节点将返回一条“no password is set”​(未设置密码)的错误提示信息。

需要注意的是,遇到任何的错误都会让从节点终止当前的复制工作,并从创建套接字开始重新执行复制,一直到身份验证通过或者从节点放弃执行复制为止。

服务器身份验证的流程图如图所示(图中的主从服务器即主从节点)​。
在这里插入图片描述

2.5、延迟与不一致

命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复,因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度与主从节点之间的网络状况、主节点写命令的执行频率以及主节点中的repl-disable-tcp-nodelay配置有关,该配置有两个值。

  • 关闭:无论数据大小都会及时同步到从节点,耗费带宽,适用于主从网络稳定的应用场景。
  • 开启:主节点每隔指定时间把数据合并为TCP包,节省带宽,默认为40毫秒同步一次。

该配置作用于命令传播阶段,控制主节点是否禁用与从节点的TCP_NODELAY,默认为no,即不禁用TCP_NODELAY。当设置为yes时,TCP会对包进行合并,从而减少对带宽的消耗,但由于发送的频率会降低,因此从节点数据延迟增加,一致性变差。具体发送频率与Linux内核的配置有关,默认配置为40毫秒。当设置为no时,TCP包会立马将主节点的数据发送给从节点,耗费带宽增加但延迟变小。一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时才会设置为yes,多数情况下使用默认设置值no。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值