Redis 集群分析-主从复制篇

829 阅读5分钟

Redis 为什么要主从复制?

  • 读写分离,提高亵渎性能
  • 数据备份,减少数据丢失的风险
  • 高可用,避免单点故障

如何复制?

一般的思路就是以主服的数据为准,从服连上主服时候先同步覆盖所有数据。然后如果主服执行了命令后,主服会把命令同步发生给从服务器。这就是我们常说的 “主写从读” 读写分离。事实上redis 2.8之前确实是这样做的。

Redis全量复制

时机:发生在从服初始化阶段

从服链接主服,发生 SYNC 命令 主服收到 SYNC 命令,开始生成 RBB 文件(一种字节码文件)并且使用缓存去记录此后执行的所有写命令,这个目的就是保存生成 RDB文件之后增量命令 主服向从服发生RBD文件,从发载入 RBD文件 主服向从服发生写缓存中增量的写命令,从服执行增量写命令

Redis增量同步:

时机:发生在从服初始化后,主服发生的写操作

增量复制就是主服执行一个写命令就会像从服发生相同的命令,从服收到并执行该命令。

说明:主从复制的是异步的,不会阻塞主、从服redis线程(这个很重要,因为redis是单线程的) 主服依然可以处理外界的命令,从服也是可以接受外界的查询,只是查到的是旧数据。

Redis 在主从模式下是如何处理 key 过期的?

从服是不会让key过期的,主服删除执行删除过期key的时候,它会合成一个del命令发生到所有的从服

int expireIfNeeded(redisDb *db, robj *key) { 
 time_t when = getExpire(db,key); 
 
 if (when < 0) return 0; /* No expire for this key */ 
 
 /* Don't expire anything while loading. It will be done later. */ 
 if (server.loading) return 0; 
 
 // 非主服不删除
 if (server.masterhost != NULL) { 
 return time(NULL) > when; 
 } 
 
 /* Return when this key has not expired */ 
 if (time(NULL) <= when) return 0; 
 
 /* Delete the key */ 
 server.stat_expiredkeys++; 
 propagateExpire(db,key); 
 return dbDelete(db,key); 
}

问题:如果从服务器不断的重连,主服就要向不断的发生全量数据

Redis 2.8之后的复制实现:

大致思路:

  • 主服维护一个偏移量当有写命令,则修改偏移量。
  • 主服维护一个固定大小的缓冲区(可配置),当收到写命令将命令写入缓冲区
  • 从服也维护一个偏移量,收到主服写命令修改偏移量。
  • 从服连上主服时候,会向主服发生上一次发送偏移量和主服id(有可能是多主)。

从服操作流程

  1. 如主服不匹配,则主服全量同步
  2. 查询该偏移量是否存在缓冲区(可能存在落后的情况,缓存区被覆盖了),此时也会进行全量同步
  3. 如果偏移量在缓冲区找到,则主服从偏移量开始,将缓冲区的写命令发生到从服

注意:主服的缓存区大小就变得非常重要,越大就会越接近全量更新而且占据内存制约 越小就会当前网络不好时,退化成全量更新。

同步代码如下:
/* replication.c: line 627 */
void syncCommand(client *c) {
    // 如果 client 是从服务器,忽略
    if (c->flags & CLIENT_SLAVE) return;
    // 如果当前也是 从服务器,并且跟 主服务器没有连接,发送错误,直接返回
    if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED) {
        addReplySds(c,sdsnew("-NOMASTERLINK Can't SYNC while not connected with my master\r\n"));
        return;
    }
    // 如果 client 缓冲区里还有数据,则不能执行
    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")) {
        // 尝试部分同步,如果成功证明不需要全量了
        if (masterTryPartialResynchronization(c) == C_OK) {
            // 可以执行PSYNC命令,则将接受PSYNC命令的个数加1
            server.stat_sync_partial_ok++;
            // 不需要全量同步了,直接返回
            return;
        } else {
        // 需要全量同步
            // 传递的命令参数
            char *master_replid = c->argv[1]->ptr;
            // 从服务器传递 ?强制全量同步,所以不能执行部分同步,所以 部分同步失败 +1
            if (master_replid[0] != '?') server.stat_sync_partial_err++;
        }
    } else {
    // sync 命令
        c->flags |= CLIENT_PRE_PSYNC;
    }
    // 全部同步统计+1
    server.stat_sync_full++;
    // 状态为 从服务器等待 BGSAVE 开始
    c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
    // 执行 sync 后是否关闭 TCP_NODELAY
    if (server.repl_disable_tcp_nodelay)
        // 启用nagle算法
        anetDisableTcpNoDelay(NULL, c->fd);
    // 保存主服务传递过来的 RDB 文件 fd,设置为 -1
    c->repldbfd = -1;
    // 标识 client 为一个从服务器
    c->flags |= CLIENT_SLAVE;
    // 将client 添加到 从服务器列表中
    listAddNodeTail(server.slaves,c);
    // 如果从服务器列表只有一个元素而且 backlog 为空,证明刚初始化
    if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) {
        // 初始化 replication id,主要是用来为后续 master-slave 模式分配唯一 id
        changeReplicationId();
        // 清除辅助 replication id,这种情况有可能发生在 在完全重新同步之后开始新的复制历史记录时
        clearReplicationId2();
        // 初始化 backlog
        createReplicationBacklog();
    }
    // 情况1: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;
            // 如果有从服务器已经创建子进程执行写RDB操作,等待完成,那么退出循环
            if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END) break;
        }
        // 对于这个从服务器,我们检查它是否具有触发当前BGSAVE操作的能力,这个也就是之前我们传递的 复制能力
        if (ln && ((c->slave_capa & slave->slave_capa) == slave->slave_capa)) {
            // 将slave的输出缓冲区所有内容拷贝给c的所有输出缓冲区中
            copyClientOutputBuffer(c,slave);
            // 设置全量重同步从服务器的状态,设置部分重同步的偏移量
            replicationSetupSlaveForFullResync(c,slave->psync_initial_offset);
            serverLog(LL_NOTICE,"Waiting for end of BGSAVE for SYNC");
        } else {
            // 从服务能力不符合条件
            serverLog(LL_NOTICE,"Can't attach the slave to the current BGSAVE. Waiting for next BGSAVE for SYNC");
        }
    // 情况2:BGSAVE 已经正在执行了,且是无盘同步,直接写到 socket 中
    } else if (server.rdb_child_pid != -1 &&
               server.rdb_child_type == RDB_CHILD_TYPE_SOCKET)
    {
        // 虽然有子进程在执行写 RDB,但是它直接写到 socket 中,不能复制,所以等待下次执行 BGSAVE
        serverLog(LL_NOTICE,"Current BGSAVE has socket target. Waiting for next BGSAVE for SYNC");
    // 情况3:BGSAVE 没有在执行
    } else {
        // 如果服务器支持无盘同步
        if (server.repl_diskless_sync && (c->slave_capa & SLAVE_CAPA_EOF)) {
            // 无盘同步复制的子进程被创建在 replicationCron() 中,因为想等待更多的从服务器可以到来而延迟,为了更多 slave 共享
            if (server.repl_diskless_sync_delay)
                serverLog(LL_NOTICE,"Delay next BGSAVE for diskless SYNC");
        } else {
        // 服务器不支持无盘复制
            // 如果没有正在执行 BGSAVE,且没有进行写 AOF文件,则开始为复制执行 BGSAVE,并且是将 RDB 文件写到磁盘上
            if (server.aof_child_pid == -1) {
                startBgsaveForReplication(c->slave_capa);
            } else {
                serverLog(LL_NOTICE,
                    "No BGSAVE in progress, but an AOF rewrite is active. BGSAVE for replication delayed");
            }
        }
    }
    return;
}

后面都会以这种笔记的形式去分析Redis的集群的~

这篇基本上就是笔记形式的老缝合怪了,别人分析得太好。我直接拿来理解就用了。(侵删)

参考文章:

www.web-lovers.com/redis-sourc…

github.com/farmerjohng…