在分布式系统中,为了解决单点问题,通常会把数据集复制多个副本部署到其他机器,以满足故障恢复和负载均衡等需求。redis 为我们提供的复制功能,实现了相同数据的多个副本,这也是其实现 HA 的基础。

参与 redis 复制功能的节点被分成两个角色,主节点(master)和从节点(slave),复制的数据流是单向的,即 master → slave
默认情况下,每个 redis 实例都是 master,mater 与 slave 的关系为 1:n(也可以没有 slave),但一个 slave 只能有一个 master。

redis 的复制功能涉及同步(sync)和命令传播(command propagate)两个阶段。
同步阶段用于将 slave 的数据库状态更新至 master 当前所处的数据库状态,即追数据阶段;
命令传播阶段则用于当 master 数据库状态改变,导致主从节点数据库状态不一致时,使之重新回到一致状态。

同步阶段

2.8 版本以前,slave 对 master 的同步,是通过 slave 向 master 发送 SYNC 命令完成的。
1)slave 向 master 发送 SYNC
2)master 收到 SYNC 后,执行 BGSAVE 命令,生成包含当前数据库状态的 RDB 文件,同时自身使用一个 buffer 记录从现在开始执行的所有改变其数据库状态的命令,RDB 生成完毕后将其发送给 slave;
3)slave 收到 RDB 文件后,载入数据,将自己的数据库状态更新至 master 执行 BGSAYE 时的状态;
4)master 将 buffer 累积的命令发给 slave;
5)slave 解析 master 发来的命令并执行,将数据追至与 master 当前所处的状态一致。

如果以上任一一步因为网络或者其他原因而中断,当 slave 再次连上 master 时,master 仍然需要重新做一个 BGSAVE,而这个命令是通过 fork 子进程来做的,频繁执行会影响性能,且复制效率低下。

为解决以上问题,redis 从 2.8 版本开始,引入新的同步命令 PSYNC 以支持断点续传。
要支持断点续传,就需要记录上次同步的位置,借助了以下三个变量:

1)master/slave 的复制偏移量(replication offset);
2)master 的复制积压缓冲区(replication backlog);
3)服务器的运行 ID(run ID)。

具体细节可以参考《redis 设计与实现》这本书的第 15 章。

命令传播阶段

redisServer 结构体中有一个 dirty 变量记录了自上一次成功执行 save 或者 bgsave 之后,数据库状态改变的次数。通过比较执行命令前后 的 dirty 值,就可以知道当前命令执行后数据库状态是否发生了改变,只有改变了才需要做 command propagate

void call(client *c, int flags) {
        ...
    dirty = server.dirty;
    start = ustime();
    c->cmd->proc(c);
    duration = ustime()-start;
    dirty = server.dirty-dirty;
    if (dirty < 0) dirty = 0;
    ...
    if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
    ...
    if (propagate_flags != PROPAGATE_NONE)
          propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
    ...
}

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{
    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
    if (flags & PROPAGATE_REPL)
        replicationFeedSlaves(server.slaves,dbid,argv,argc);
}

对于主从复制的命令传播,在 replicationFeedSlaves 函数中实现。

void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
    listNode *ln;
    listIter li;
    int j, len;
    char llstr[LONG_STR_SIZE];

    // 如果 backlog buffer 为空,且没有 slave,直接返回
    if (server.repl_backlog == NULL && listLength(slaves) == 0) return;
    serverAssert(!(listLength(slaves) != 0 && server.repl_backlog == NULL));

    // 如果 dictid 与上一次 repl 选择的不一致,需要插入一条 select 命令
    if (server.slaveseldb != dictid) {
        robj *selectcmd;
        ......
    }
    server.slaveseldb = dictid;

    // 将命令以 redis 协议的格式写入 replication backlog
    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++) {
            // $..CRLF
            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); // CRLF
        }
    }

    /* 将命令发送给所有的 slave. */
    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        client *slave = ln->value;
      
        /* Don't feed slaves that are still waiting for BGSAVE to start */
        if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) continue;
        // 以 redis 协议的格式发送给 slave
        addReplyMultiBulkLen(slave,argc);
        for (j = 0; j < argc; j++)
            addReplyBulk(slave,argv[j]);
    }
}

以上便是主从同步的两个阶段,更多相关代码详解请看后面的博客分析。


happen
341 声望111 粉丝

几句话没办法介绍自己...