主从复制:基于状态机的设计与实现

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 源码剖析与实战》

相关推荐
姜学迁33 分钟前
Rust-枚举
开发语言·后端·rust
【D'accumulation】1 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
2401_854391081 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss1 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
Cikiss2 小时前
微服务实战——平台属性
java·数据库·后端·微服务
OEC小胖胖2 小时前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web
2401_857617622 小时前
SpringBoot校园资料平台:开发与部署指南
java·spring boot·后端
计算机学姐2 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
Yvemil73 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
2401_854391083 小时前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端