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(有可能是多主)。
从服操作流程
- 如主服不匹配,则主服全量同步
- 查询该偏移量是否存在缓冲区(可能存在落后的情况,缓存区被覆盖了),此时也会进行全量同步
- 如果偏移量在缓冲区找到,则主服从偏移量开始,将缓冲区的写命令发生到从服
注意:主服的缓存区大小就变得非常重要,越大就会越接近全量更新而且占据内存制约 越小就会当前网络不好时,退化成全量更新。
同步代码如下:
/* 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的集群的~
这篇基本上就是笔记形式的老缝合怪了,别人分析得太好。我直接拿来理解就用了。(侵删)
参考文章: