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

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

相关推荐
bearpping28 分钟前
SpringBoot最佳实践之 - 使用AOP记录操作日志
java·spring boot·后端
一叶飘零_sweeeet30 分钟前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·后端·spring
开心就好20252 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默2 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦2 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl2 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6863 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情3 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player3 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明3 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展