Redis 的主从复制主要包括了全量复制、增量复制和长连接同步三种情况。全量复制传输 RDB 文件,增量复制传输主从断连期间的命令,而长连接同步则是把主节点正常收到的请求传输给从节点。
Redis 是采用了基于状态机的设计思想,来清晰地实现不同状态及状态间的跳转。
主从复制的四大阶段
分别是初始化、建立连接、主从握手、复制类型判断与执行。
1. 初始化阶段
当我们把一个 Redis 实例 A 设置为另一个实例 B 的从库时,实例 A 会完成初始化操作,主要是获得了主库的 IP 和端口号。
2. 建立连接阶段
一旦实例 A 获得了主库 IP 和端口号,该实例就会尝试和主库建立 TCP 网络连接,并且会在建立好的网络连接上,监听是否有主库发送的命令。
3.主从握手阶段
当实例 A 和主库建立好连接之后,实例 A 就开始和主库进行握手。简单来说,握手过程就是主从库间相互发送 PING-PONG 消息,同时从库根据配置信息向主库进行验证。最后,从库把自己的 IP、端口号,以及对无盘复制和 PSYNC 2 协议的支持情况发给主库。
4. 复制类型判断与执行阶段
等到主从库之间的握手完成后,从库就会给主库发送 PSYNC 命令。紧接着,主库会根据从库发送的命令参数作出相应的三种回复,分别是执行全量复制、执行增量复制、发生错误。最后,从库在收到上述回复后,就会根据回复的复制类型,开始执行具体的复制操作。
基于状态机的主从复制实现
每一个 Redis 实例在代码中都对应一个 redisServer 结构体,这个结构体包含了和 Redis 实例相关的各种配置,比如实例的 RDB、AOF 配置、主从复制配置、切片集群配置等。然后,与主从复制状态机相关的变量是 repl_state,Redis 在进行主从复制时,从库就是根据这个变量值的变化,来实现不同阶段的执行和跳转。
arduino
struct redisServer {
...
/* 复制相关(slave) */
char *masterauth; /* 用于和主库进行验证的密码*/
char *masterhost; /* 主库主机名 */
int masterport; /* 主库端口号r */
...
client *master; /* 从库上用来和主库连接的客户端 */
client *cached_master; /* 从库上缓存的主库信息 */
int repl_state; /* 从库的复制状态机 */
...
}
初始化阶段
当一个实例启动后,就会调用 server.c 中的 initServerConfig 函数,初始化 redisServer 结构体。此时,实例会把状态机的初始状态设置为 REPL_STATE_NONE,如下所示:
javascript
void initServerConfig(void) {
...
server.repl_state = REPL_STATE_NONE;
...
}
一旦实例执行了 replicaof masterip masterport 命令,就会调用 replication.c 中的 replicaofCommand 函数进行处理。replicaof 命令携带的 masterip 和 masterport 参数对应了主库的 IP 和端口号,replicaofCommand 函数如果判断发现实例并没有记录过主库的 IP 和端口号,就表明当前实例可以和设置的主库进行连接。
紧接着,replicaofCommand 函数会调用 replicationSetMaster 函数设置主库的信息。这部分的代码逻辑如下所示:
rust
/* 检查是否已记录主库信息,如果已经记录了,那么直接返回连接已建立的消息 */
if (server.masterhost && !strcasecmp(server.masterhost,c->argv[1]->ptr)&& server.masterport == port) {
serverLog(LL_NOTICE,"REPLICAOF 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和端口号,设置主库的信息 */
replicationSetMaster(c->argv[1]->ptr, port);
而 replicationSetMaster 函数除了会记录主库的 IP、端口号之外,还会把从库实例的状态机设置为 REPL_STATE_CONNECT。此时,主从复制的初始化阶段就完成了,状态机会从 REPL_STATE_NONE 变迁为 REPL_STATE_CONNECT。
建立连接阶段
从库是何时开始和主库建立网络连接的呢?和周期任务有关。
replicationCron() ,这个任务的执行频率是每 1000ms 执行一次。replicationCron() 任务的函数实现逻辑是在 server.c 中,在该任务中,一个重要的判断就是,检查从库的复制状态机状态。如果状态机状态是 REPL_STATE_CONNECT,那么从库就开始和主库建立连接。连接的建立是通过调用 connectWithMaster() 函数来完成的。
当从库实例调用 connectWithMaster 函数后,会先通过 anetTcpNonBlockBestEffortBindConnect 函数和主库建立连接。一旦连接建立成功后,从库实例就会在连接上创建读写事件,并且注册对读写事件进行处理的函数 syncWithMaster。最后,connectWithMaster 函数会将从库实例的状态机置为 REPL_STATE_CONNECTING。
scss
int connectWithMaster(void) {
int fd;
//从库和主库建立连接
fd = anetTcpNonBlockBestEffortBindConnect(NULL, server.masterhost,server.masterport,NET_FIRST_BIND_ADDR);
...
//在建立的连接上注册读写事件,对应的回调函数是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;
}
//完成连接后,将状态机设置为REPL_STATE_CONNECTING
...
server.repl_state = REPL_STATE_CONNECTING;
return C_OK;
}
主从握手阶段
当主从库建立网络连接后,从库实例其实并没有立即开始进行数据同步,而是会先和主库之间进行握手通信。
握手通信的目的,主要包括从库和主库进行验证,以及从库将自身的 IP 和端口号发给主库。
一旦主库和从库的连接建立后,从库实例的 syncWithMaster 函数就会被回调。在这个函数中,如果从库实例的状态是 REPL_STATE_CONNECTING,那么实例会发送 PING 消息给主库,并将状态机置为 REPL_STATE_RECEIVE_PONG。
当从库收到主库返回的 PONG 消息后,接下来,从库会依次给主库发送验证信息、端口号、IP、对 RDB 文件和无盘复制的支持情况。每一次的握手通信发送消息时,都会对应从库的一组状态变迁。
比如,当从库要给主库发送验证信息前,会将自身状态机置为 REPL_STATE_SEND_AUTH,然后,从库给主库发送实际的验证信息。验证信息发送完成后,从库状态机会变迁为 REPL_STATE_RECEIVE_AUTH,并开始读取主库返回验证结果信息。
复制类型判断与执行阶段
当从库和主库完成握手后,从库会读取主库返回的 CAPA 消息响应,此时,状态机为 REPL_STATE_RECEIVE_CAPA。紧接着,从库的状态变迁为 REPL_STATE_SEND_PSYNC,表明要开始向主库发送 PSYNC 命令,开始实际的数据同步。
此时,从库会调用 slaveTryPartialResynchronization 函数,向主库发送 PSYNC 命令,并且状态机的状态会置为 REPL_STATE_RECEIVE_PSYNC。下面的代码显示了这三个状态的变迁:
ini
/* 从库状态机进入REPL_STATE_RECEIVE_CAPA. */
if (server.repl_state == REPL_STATE_RECEIVE_CAPA) {
...
//读取主库返回的CAPA消息响应
server.repl_state = REPL_STATE_SEND_PSYNC;
}
//从库状态机变迁为REPL_STATE_SEND_PSYNC后,开始调用slaveTryPartialResynchronization函数向主库发送PSYNC命令,进行数据同步
if (server.repl_state == REPL_STATE_SEND_PSYNC) {
if (slaveTryPartialResynchronization(fd,0) == PSYNC_WRITE_ERROR)
{
...
}
server.repl_state = REPL_STATE_RECEIVE_PSYNC;
return;
}
从库调用的 slaveTryPartialResynchronization 函数,负责向主库发送数据同步的命令。主库收到命令后,会根据从库发送的主库 ID、复制进度值 offset,来判断是进行全量复制还是增量复制,或者是返回错误。
arduino
int slaveTryPartialResynchronization(int fd, int read_reply) {
...
//发送PSYNC命令
if (!read_reply) {
//从库第一次和主库同步时,设置offset为-1
server.master_initial_offset = -1;
...
//调用sendSynchronousCommand发送PSYNC命令
reply =
sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_replid,psync_offset,NULL);
...
//发送命令后,等待主库响应
return PSYNC_WAIT_REPLY;
}
//读取主库的响应
reply = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
//主库返回FULLRESYNC,全量复制
if (!strncmp(reply,"+FULLRESYNC",11)) {
...
return PSYNC_FULLRESYNC;
}
//主库返回CONTINUE,执行增量复制
if (!strncmp(reply,"+ CONTINUE",11)) {
...
return PSYNC_CONTINUE;
}
//主库返回错误信息
if (strncmp(reply,"-ERR",4)) {
...
}
return PSYNC_NOT_SUPPORTED;
}
slaveTryPartialResynchronization 是在 syncWithMaster 函数中调用的,当该函数返回 PSYNC 命令不同的结果时,syncWithMaster 函数就会根据结果值执行不同处理。
当主库对从库的 PSYNC 命令返回 FULLRESYNC 时,从库会在和主库的网络连接上注册 readSyncBulkPayload 回调函数,并将状态机置为 REPL_STATE_TRANSFER,表示开始进行实际的数据同步,比如主库把 RDB 文件传输给从库。
此文章为10月Day21学习笔记,内容来源于极客时间《Redis 源码剖析与实战》