Redis 复制(replicate)实现
1. 复制的介绍
Redis
为了解决单点数据库问题,会把数据复制多个副本部署到其他节点上,通过复制,实现Redis
的高可用性,实现对数据的冗余备份,保证数据和服务的高度可靠性。
关于复制的详细配置和如何建立复制,请参考:Redis 复制功能详解 。
2. 复制的实现
本文主要剖析:
- 第一次执行复制所进行全量同步的全过程
- 部分重同步的实现
replication.c
文件详细注释:Redis 复制代码注释
2.1 主从关系的建立
复制的建立方法有三种。
- 在
redis.conf
文件中配置slaveof <masterip> <masterport>
选项,然后指定该配置文件启动Redis
生效。 - 在
redis-server
启动命令后加上--slaveof <masterip> <masterport>
启动生效。 - 直接使用
slaveof <masterip> <masterport>
命令在从节点执行生效。
无论是通过哪一种方式来建立主从复制,都是从节点来执行slaveof
命令,那么从节点执行了这个命令到底做了什么,我们上源码:
// SLAVEOF host port命令实现
void slaveofCommand(client *c) {
// 如果当前处于集群模式,不能进行复制操作
if (server.cluster_enabled) {
addReplyError(c,"SLAVEOF not allowed in cluster mode.");
return;
}
// SLAVEOF NO ONE命令使得这个从节点关闭复制功能,并从从节点转变回主节点,原来同步所得的数据集不会被丢弃。
if (!strcasecmp(c->argv[1]->ptr,"no") &&
!strcasecmp(c->argv[2]->ptr,"one")) {
// 如果保存了主节点IP
if (server.masterhost) {
// 取消复制操作,设置服务器为主服务器
replicationUnsetMaster();
// 获取client的每种信息,并以sds形式返回,并打印到日志中
sds client = catClientInfoString(sdsempty(),c);
serverLog(LL_NOTICE,"MASTER MODE enabled (user request from '%s')",
client);
sdsfree(client);
}
// SLAVEOF host port
} else {
long port;
// 获取端口号
if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != C_OK))
return;
// 如果已存在从属于masterhost主节点且命令参数指定的主节点和masterhost相等,端口也相等,直接返回
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;
}
// 第一次执行设置端口和ip,或者是重新设置端口和IP
// 设置服务器复制操作的主节点IP和端口
replicationSetMaster(c->argv[1]->ptr, port);
// 获取client的每种信息,并以sds形式返回,并打印到日志中
sds client = catClientInfoString(sdsempty(),c);
serverLog(LL_NOTICE,"SLAVE OF %s:%d enabled (user request from '%s')",
server.masterhost, server.masterport, client);
sdsfree(client);
}
// 回复ok
addReply(c,shared.ok);
}
当从节点的client执行SLAVEOF
命令后,该命令会被构建成Redis
协议格式,发送给从节点服务器,然后节点服务器会调用slaveofCommand()
函数执行该命令。
具体的命令接受和回复请参考:Redis 网络连接库剖析
而SLAVEOF
命令做的操作并不多,主要以下三步:
- 判断当前环境是否在集群模式下,因为集群模式下不行执行该命令。
- 是否执行的是
SLAVEOF NO ONE
命令,该命令会断开主从的关系,设置当前节点为主节点服务器。 - 设置从节点所属主节点的
IP
和port
。调用了replicationSetMaster()
函数。
SLAVEOF
命令能做的只有这么多,我们来具体看下replicationSetMaster()
函数的代码,看看它做了哪些与复制相关的操作。
// 设置复制操作的主节点IP和端口
void replicationSetMaster(char *ip, int port) {
// 按需清除原来的主节点信息
sdsfree(server.masterhost);
// 设置ip和端口
server.masterhost = sdsnew(ip);
server.masterport = port;
// 如果有其他的主节点,在释放
// 例如服务器1是服务器2的主节点,现在服务器2要同步服务器3,服务器3要成为服务器2的主节点,因此要释放服务器1
if (server.master) freeClient(server.master);
// 解除所有客户端的阻塞状态
disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */
// 关闭所有从节点服务器的连接,强制从节点服务器进行重新同步操作
disconnectSlaves(); /* Force our slaves to resync with us as well. */
// 释放主节点结构的缓存,不会执行部分重同步PSYNC
replicationDiscardCachedMaster(); /* Don't try a PSYNC. */
// 释放复制积压缓冲区
freeReplicationBacklog(); /* Don't allow our chained slaves to PSYNC. */
// 取消执行复制操作
cancelReplicationHandshake();
// 设置复制必须重新连接主节点的状态
server.repl_state = REPL_STATE_CONNECT;
// 初始化复制的偏移量
server.master_repl_offset = 0;
// 清零连接断开的时长
server.repl_down_since = 0;
}
由代码知,replicationSetMaster()
函数执行操作的也很简单,总结为两步:
- 清理之前所属的主节点的信息。
- 设置新的主节点
IP
和port
等。
因为,当前从节点有可能之前从属于另外的一个主节点服务器,因此要清理所有关于之前主节点的缓存、关闭旧的连接等等。然后设置该从节点的新主节点,设置了IP
和port
,还设置了以下状态:
// 设置复制必须重新连接主节点的状态
server.repl_state = REPL_STATE_CONNECT;
// 初始化全局复制的偏移量
server.master_repl_offset = 0;
然后,就没有然后了,然后就会执行复制操作吗?这也没有什么关于复制的操作执行了,那么复制操作是怎么开始的呢?
2.2 主从网络连接建立
slaveof
命令是一个异步命令,执行命令时,从节点保存主节点的信息,确立主从关系后就会立即返回,后续的复制流程在节点内部异步执行。那么,如何触发复制的执行呢?
周期性执行的函数:replicationCron()
函数,该函数被服务器的时间事件的回调函数serverCron()
所调用,而serverCron()
函数在Redis
服务器初始化时,被设置为时间事件的处理函数。
// void initServer(void) Redis服务器初始化
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)
replicationCron()
函数执行频率为1秒一次:
// 节选自serverCron函数
// 周期性执行复制的任务
run_with_period(1000) replicationCron();
主从关系建立后,从节点服务器的server.repl_state
被设置为REPL_STATE_CONNECT
,而replicationCron()
函数会被每秒执行一次,该函数会发现我(从节点)现在有主节点了,而且我要的状态是要连接主节点(REPL_STATE_CONNECT)。
replicationCron()
函数处理这以情况的代码如下:
/* Check if we should connect to a MASTER */
// 如果处于要必须连接主节点的状态,尝试连接
if (server.repl_state == REPL_STATE_CONNECT) {
serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
server.masterhost, server.masterport);
// 以非阻塞的方式连接主节点
if (connectWithMaster() == C_OK) {
serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started");
}
}
replicationCron()
函数根据从节点的状态,调用connectWithMaster()
非阻塞连接主节点。代码如下:
// 以非阻塞的方式连接主节点
int connectWithMaster(void) {
int fd;
// 连接主节点
fd = anetTcpNonBlockBestEffortBindConnect(NULL,
server.masterhost,server.masterport,NET_FIRST_BIND_ADDR);
if (fd == -1) {
serverLog(LL_WARNING,"Unable to connect to MASTER: %s",
strerror(errno));
return C_ERR;
}
// 监听主节点fd的可读和可写事件的发生,并设置其处理程序为syncWithMaster
if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) == AE_ERR)
{
close(fd);
serverLog(LL_WARNING,"Can't create readable event for SYNC");
return C_ERR;
}
// 最近一次读到RDB文件内容的时间
server.repl_transfer_lastio = server.unixtime;
// 从节点和主节点的同步套接字
server.repl_transfer_s = fd;
// 处于和主节点正在连接的状态
server.repl_state = REPL_STATE_CONNECTING;
return C_OK;
}
connectWithMaster()
函数执行的操作可以总结为:
- 根据
IP
和port
非阻塞的方式连接主节点,得到主从节点进行通信的文件描述符fd
,并保存到从节点服务器server.repl_transfer_s
中,并且将刚才的REPL_STATE_CONNECT
状态设置为REPL_STATE_CONNECTING
。 - 监听
fd
的可读和可写事件,并且设置事件发生的处理程序syncWithMaster()
函数。
至此,主从网络建立就完成了。
2.3 发送PING命令
主从建立网络时,同时注册fd
的AE_READABLE|AE_WRITABLE
事件,因此会触发一个AE_WRITABLE
事件,调用syncWithMaster()
函数,处理写事件。
根据当前的REPL_STATE_CONNECTING
状态,从节点向主节点发送PING
命令,PING
命令的目的有:
- 检测主从节点之间的网络是否可用。
- 检查主从节点当前是否接受处理命令。
syncWithMaster()
函数中相关操作的代码如下:
/* Send a PING to check the master is able to reply without errors. */
// 如果复制的状态为REPL_STATE_CONNECTING,发送一个PING去检查主节点是否能正确回复一个PONG
if (server.repl_state == REPL_STATE_CONNECTING) {
serverLog(LL_NOTICE,"Non blocking connect for SYNC fired the event.");
// 暂时取消接听fd的写事件,以便等待PONG回复时,注册可读事件
aeDeleteFileEvent(server.el,fd,AE_WRITABLE);
// 设置复制状态为等待PONG回复
server.repl_state = REPL_STATE_RECEIVE_PONG;
// 发送一个PING命令
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PING",NULL);
if (err) goto write_error;
return;
}
发送PING
命令主要的操作是:
- 先取消监听
fd
的写事件,因为接下来要读主节点服务器发送过来的PONG
回复,因此只监听可读事件的发生。 - 设置从节点的复制状态为
REPL_STATE_RECEIVE_PONG
。等待一个主节点回复一个PONG
命令。 - 以写的方式调用
sendSynchronousCommand()
函数发送一个PING
命令给主节点。
主节点服务器从fd
会读到一个PING
命令,然后会回复一个PONG
命令到fd
中,执行的命令就是addReply(c,shared.pong);
。
此时,会触发fd
的可读事件,调用syncWithMaster()
函数来处理,此时从节点的复制状态为REPL_STATE_RECEIVE_PONG
,等待主节点回复PONG
。syncWithMaster()
函数中处理这一状态的代码如下:
/* Receive the PONG command. */
// 如果复制的状态为REPL_STATE_RECEIVE_PONG,等待接受PONG命令
if (server.repl_state == REPL_STATE_RECEIVE_PONG) {
// 从主节点读一个PON