redis架构设计: redis-server的启动(硬核分析)

怎么在windows上用clion搭建redis的源码阅读环境
请看我的上一篇文章

redis启动之后都干了什么呢? 我们知道,redis的服务端对应的源码位置是server.c

main函数是程序启动的入口 ,下面我来一行一行的分析server.c的源码

1、定义时间函数变量

struct timeval tv;
int j;

timeval是C语言定义的一个结构体,里面有2个值

tv_sec 表示秒数,tv_usec 表示微秒数

这个结构体通常用于记录时间,因为它可以精确到微秒级别。

2、预编译指令:条件测试&进程标题

#ifdef REDIS_TEST
    if (argc == 3 && !strcasecmp(argv[1], "test")) {
        if (!strcasecmp(argv[2], "ziplist")) {
            return ziplistTest(argc, argv);
        } else if (!strcasecmp(argv[2], "quicklist")) {
            quicklistTest(argc, argv);
        } else if (!strcasecmp(argv[2], "intset")) {
            return intsetTest(argc, argv);
        } else if (!strcasecmp(argv[2], "zipmap")) {
            return zipmapTest(argc, argv);
        } else if (!strcasecmp(argv[2], "sha1test")) {
            return sha1Test(argc, argv);
        } else if (!strcasecmp(argv[2], "util")) {
            return utilTest(argc, argv);
        } else if (!strcasecmp(argv[2], "endianconv")) {
            return endianconvTest(argc, argv);
        } else if (!strcasecmp(argv[2], "crc64")) {
            return crc64Test(argc, argv);
        } else if (!strcasecmp(argv[2], "zmalloc")) {
            return zmalloc_test(argc, argv);
        }

        return -1; /* test not found */
    }
#endif

    // 预编译指令-进程标题
    // 在某些情况下,我们可能希望进程标题能够更直观地反映 Redis 的用途,这时可以使用自定义的进程标题设置函数来实现。
#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif

#ifdef 指令是一个条件编译指令,用于判断某个宏是否已经被定义过

REDIS_TEST:预编译指令,条件测试,主要用于判断当前是否处于测试模式下

Redis 中,测试模式下的代码与正式发布版本的代码可能存在一些差异,因此需要使用条件编译指令进行区分。

INIT_SETPROCTITLE_REPLACEMENT:编译指令-进程标题

在某些情况下,我们可能希望进程标题能够更直观地反映 Redis 的用途,这时可以使用自定义的进程标题设置函数来实现。

3、设置本地化信息

   setlocale(LC_COLLATE, "");

这个是C语言标准库的函数,用于设置程序本地信息

该函数使用两个参数,第一个参数指定要更改的环境变量类型,第二个参数为要设置的值。

在这里,第一个参数为LC_COLLATE,它指定要更改的环境变量类型为排序规则。第二个参数为空字符串,表示将排序规则设置为默认值。

4、设置时区

   tzset();

C 标准库中的一个函数,用于初始化时区信息。

在代码中调用tzset()函数可以保证程序在处理时间相关操作时使用正确的时区信息,避免了因时区设置不正确而导致的时间计算错误。

5、内存分配时候的回调函数

zmalloc_set_oom_handler(redisOutOfMemoryHandler);

这段代码的作用是设置一个函数(redisOutOfMemoryHandler)来处理内存不足的情况。

最终的效果是,当程序运行时出现内存不足的情况,就会调用redisOutOfMemoryHandler函数来处理。

Redis 实现在内存分配失败时能够执行错误处理函数的关键在于两个方面:

  1. 使用了 zmalloc 函数库作为 Redis 的内存分配器:Redis 使用 zmalloc 作为内存分配器而不是 C 语言标准库中的 malloc 函数。zmalloc 函数库是一个基于 malloc 的封装,它在 malloc 的基础上添加了一些特性,例如内存分配跟踪、内存对齐等。同时,zmalloc 还提供了 zmalloc_set_oom_handler 函数,它能够让开发者注册一个函数,当 Redis 在内存分配失败时会调用该函数。
  2. zmalloc 函数库中,重写了 mallocfree 函数:zmalloc 函数库重写了 C 语言标准库中的 mallocfree 函数,以实现自己的内存分配器。在 zmalloc 中,重写的 malloc 函数在内存分配失败时不是返回 NULL,而是调用了错误处理函数。而且在 zmalloc 中还提供了一个 zmalloc_oom 函数,它会触发 Redis 的 OOM 事件,这个事件将会使 Redis 执行错误处理函数

6、初始化随机数发生器

    // 下面4行是初始随机数发生器
    // 这些操作的目的是为了在 Redis 运行期间产生各种随机数,以及在需要进行校验和计算时使用 CRC64 校验表。这样可以增加 Redis 的安全性和可靠性,确保 Redis 在各种情况下都能正常运行。
    // 使用当前进程的进程 ID、时间戳、以及微秒数作为种子,通过 srand 函数初始化随机数发生器。
    srand(time(NULL) ^ getpid());
    // 调用 gettimeofday 函数获取当前时间,并存储到 tv 结构体中。
    gettimeofday(&tv, NULL);
    // 使用当前时间的秒数和微秒数、以及进程 ID 作为种子,初始化一个 64 位随机数发生器。
    init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid());
    // 调用 crc64_init 函数初始化 CRC64 校验表。
    crc64_init();

7、Redis操作系统的文件权限

  umask(server.umask = umask(0777));

umask 函数的作用是屏蔽掉进程的某些权限,比如读、写、执行等权限,使其不能被文件创建系统调用所继承。
在 Redis 中,通过将文件创建屏蔽字设置为 0777,可以确保 Redis 在创建文件时不会受到任何限制,这对于一些需要频繁创建和操作文件的场景非常重要,比如 AOF 持久化和 RDB 持久化

8、哈希种子

    uint8_t hashseed[16];
    getRandomBytes(hashseed, sizeof(hashseed));
    dictSetHashFunctionSeed(hashseed);

这段代码的作用是生成一个随机的 16 字节的哈希种子,并将其传递给字典的哈希函数。

哈希函数的种子是一个常量,它影响到哈希函数的散列结果,用于减少哈希冲突,提高哈希表的效率。

9、检验哨兵模式

server.sentinel_mode = checkForSentinelMode(argc, argv);

10、初始化配置

initServerConfig();

这个步骤是核心的步骤

是Redis初始化服务器配置结构体的函数

在该函数中,会将服务器配置结构体的各个成员变量设置为默认值,然后根据启动参数以及配置文件的设置修改这些默认值。

11、初始化访问控制列表

    ACLInit();

这段代码调用了ACLInit()函数,该函数的作用是初始化ACL(访问控制列表)系统。

说白了就是redis权限相关的配置

12、初始化redis的模块系统

moduleInitModulesSystem();

moduleInitModulesSystem() 函数的作用是初始化 Redis 模块系统,它会遍历所有已经加载的 Redis 模块,调用它们的初始化函数,完成模块的注册和初始化工作。在 Redis 4.0 中引入模块化架构之后,通过 Redis 模块系统,用户可以自定义 Redis 的功能,实现自己的 扩展和定制 ,而 moduleInitModulesSystem() 函数则是模块系统的核心函数之一,它保证了 Redis 模块的正常运行。

13、初始化 Redis 的 TLS/SSL 支持

 tlsInit();

tlsInit()函数是Redis服务器TLS(传输层安全性)支持的一部分。它在服务器初始化期间调用,用于设置TLS上下文。

tlsInit() 函数的作用是初始化 Redis 的 TLS/SSL 支持,它会启动 TLS/SSL 服务,为 Redis 服务器提供加密通信的能力。在 Redis 6.0 版本中,引入了对 TLS/SSL 的支持,用户可以通过配置文件启用 TLS/SSL 服务,以保证 Redis 服务器与客户端之间的通信安全。而 redis tlsInit() 函数则是 TLS/SSL 支持的核心函数之一,它会初始化 TLS/SSL 相关的数据结构和参数,为 Redis 服务器启动 TLS/SSL 服务做好准备。

14、初始化命令行参数

    server.executable = getAbsolutePath(argv[0]);
    server.exec_argv = zmalloc(sizeof(char *) * (argc + 1));
    server.exec_argv[argc] = NULL;
    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);

这段代码的作用是将命令行参数argv保存到Redis服务器的全局变量server.exec_argv中,以便在Redis服务器启动时使用。

具体来说,这段代码做了以下几件事情:

  • 调用getAbsolutePath函数获取Redis服务器可执行文件的绝对路径,并将结果保存到server.executable变量中;

  • 使用zmalloc函数动态分配一段内存,用于保存命令行参数argv的拷贝;

  • 将server.exec_argv数组的最后一个元素设置为NULL,以便在后续处理中使用;

  • 使用zstrdup函数将每个命令行参数argv[j]的拷贝保存到server.exec_argv[j]中。
    总的来说,这段代码的作用是为Redis服务器初始化命令行参数,以便在启动时使用。

15、 初始化哨兵配置并启动哨兵。

    // 如果服务器处于哨兵模式,则初始化哨兵配置并启动哨兵。
    // 这里所说的 Sentinel 是 Redis 高可用方案中的一个组件,用于实现自动故障转移等功能。
    if (server.sentinel_mode) {
        // 初始化哨兵配置,包括哨兵的端口、IP地址、哨兵监控的主服务器等
        initSentinelConfig();
        // 启动哨兵进程,监听主服务器的状态变化,并在主服务器出现故障时进行故障转移操作
        initSentinel();
    }

16、检查RDB或者AOF文件

    if (strstr(argv[0], "redis-check-rdb") != NULL)
        redis_check_rdb_main(argc, argv, NULL);
    else if (strstr(argv[0], "redis-check-aof") != NULL)
        redis_check_aof_main(argc, argv);

这段代码是Redis中的一个命令处理函数,它用于检查RDB文件或AOF文件的完整性和正确性。具体来说,它会根据命令行参数中的文件路径,读取相应的文件,并对其进行检查。如果文件存在问题,它会输出错误信息并退出程序。

这也是Redis对于RDB文件或AOF文件是否完整和正确的一个预检查机制,以保证Redis的数据不会受到损坏或丢失。

17、检测并且解析用户的命令行参数

17.1 版本号

  if (strcmp(argv[1], "-v") == 0 ||
            strcmp(argv[1], "--version") == 0) version();

如果传递了版本号的参数,则打印版本号的信息

oid version(void) {
    printf("Redis server v=%s sha=%s:%d malloc=%s bits=%d build=%llx\n",
           REDIS_VERSION,
           redisGitSHA1(),
           atoi(redisGitDirty()) > 0,
           ZMALLOC_LIB,
           sizeof(long) == 4 ? 32 : 64,
           (unsigned long long) redisBuildId());
    exit(0);
}

17.2 帮助

 if (strcmp(argv[1], "--help") == 0 ||
            strcmp(argv[1], "-h") == 0) usage();

如果传递了帮助的参数,则开启redis的帮助

void usage(void) {
    fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf] [options]\n");
    fprintf(stderr, "       ./redis-server - (read config from stdin)\n");
    fprintf(stderr, "       ./redis-server -v or --version\n");
    fprintf(stderr, "       ./redis-server -h or --help\n");
    fprintf(stderr, "       ./redis-server --test-memory \n\n");
    fprintf(stderr, "Examples:\n");
    fprintf(stderr, "       ./redis-server (run the server with default conf)\n");
    fprintf(stderr, "       ./redis-server /etc/redis/6379.conf\n");
    fprintf(stderr, "       ./redis-server --port 7777\n");
    fprintf(stderr, "       ./redis-server --port 7777 --replicaof 127.0.0.1 8888\n");
    fprintf(stderr, "       ./redis-server /etc/myredis.conf --loglevel verbose\n\n");
    fprintf(stderr, "Sentinel mode:\n");
    fprintf(stderr, "       ./redis-server /etc/sentinel.conf --sentinel\n");
    exit(1);
}

该代码定义了一个名为“usage”的函数,该函数将打印出Redis服务器的使用说明。该函数将在命令行参数错误或用户请求帮助时被调用。该函数将向标准错误流打印出一些使用示例和选项

最后, exit(1) 退出程序并返回1

17.3 test-memory

if (strcmp(argv[1], "--test-memory") == 0) {
    if (argc == 3) {
        memtest(atoi(argv[2]), 50);
        exit(0);
    } else {
        fprintf(stderr, "Please specify the amount of memory to test in megabytes.\n");
        fprintf(stderr, "Example: ./redis-server --test-memory 4096\n\n");
        exit(1);
    }
}

这段代码的作用是检测Redis服务器的内存是否正常工作

命令行参数为“–test-memory”,则会调用memtest函数测试内存,如果有两个参数,则第二个参数指定要测试的内存大小,然后退出程序;否则,会输出错误信息并退出程序。

17.4 redis的配置文件路径

if (argv[j][0] != '-' || argv[j][1] != '-') {
    configfile = argv[j];
    server.configfile = getAbsolutePath(configfile);
    /* Replace the config file in server.exec_argv with
     * its absolute path. */
    zfree(server.exec_argv[j]);
    server.exec_argv[j] = zstrdup(server.configfile);
    j++;
}

在Redis中,对命令行参数进行处理,如果当前参数不是以"–"开头的选项参数,则将其作为配置文件路径处理,并将其转换为绝对路径,然后替换掉server.exec_argv中的相应参数。

具体步骤如下:

  1. 判断当前命令行参数(argv[j])是否以"–"开头,如果不是则执行以下步骤。
  2. 将configfile赋值为argv[j],即为命令行参数中的配置文件路径。
  3. 调用getAbsolutePath函数将configfile转换为绝对路径,并将结果赋值给server.configfile。
  4. 释放server.exec_argv[j]的内存空间,然后将server.configfile的副本赋值给server.exec_argv[j]。
  5. 将j的值加1(原来1 变成了2)。

17.5 将用户的命令行参数转换为字符串

while (j != argc) {
    if (argv[j][0] == '-' && argv[j][1] == '-') {
        /* Option name */
        if (!strcmp(argv[j], "--check-rdb")) {
            /* Argument has no options, need to skip for parsing. */
            j++;
            continue;
        }
        if (sdslen(options)) options = sdscat(options, "\n");
        options = sdscat(options, argv[j] + 2);
        options = sdscat(options, " ");
    } else {
        /* Option argument */
        options = sdscatrepr(options, argv[j], strlen(argv[j]));
        options = sdscat(options, " ");
    }
    j++;
}

这段代码是Redis命令行解析器中的一部分,它的作用是将命令行参数解析为选项和选项参数,并将它们存储在一个字符串中。具体来说,它的实现步骤如下:

  1. 使用while循环遍历命令行参数数组,从第一个参数开始遍历,直到最后一个参数。
  2. 如果当前参数是以"–"开头的选项名,则将其添加到选项字符串中,并在后面添加一个空格。
  3. 如果当前参数不是选项名,则将其解析为选项参数,并使用sdscatrepr函数将其添加到选项字符串中,然后在后面添加一个空格。
  4. 使用j++将指针移到下一个参数,继续解析下一个参数。
  5. 循环结束后,选项字符串中将包含所有的选项和选项参数,它们以空格分隔。这个字符串将被传递给Redis的其他函数,用于执行具体的命令操作。

18、检查哨兵模式和配置文件

if (server.sentinel_mode && configfile && *configfile == '-') {
    serverLog(LL_WARNING,
              "Sentinel config from STDIN not allowed.");
    serverLog(LL_WARNING,
              "Sentinel needs config file on disk to save state.  Exiting...");
    exit(1);
}

这段代码的作用是在Redis Sentinel模式下检查配置文件是否从标准输入中读取,如果是,则输出警告信息并退出程序

19、重置服务器保存参数

resetServerSaveParams(); 

redis6.0中的resetServerSaveParams函数是用于重置服务器保存参数的函数。

它的主要作用是释放服务器保存参数的内存空间,将服务器保存参数指针设置为NULL,并将服务器保存参数长度设置为0,以便在下一次保存操作时重新设置保存参数。这个函数通常在服务器启动时调用,以确保服务器保存参数的初始状态是正确的。

在redis6.0中,服务器保存参数被用于控制数据持久化的方式,包括RDB快照和AOF日志等。通过重置服务器保存参数,可以确保数据持久化的方式是正确的,并且可以避免一些潜在的问题,如内存泄漏和指针悬挂等。因此,resetServerSaveParams函数在redis6.0中具有重要的意义。

void resetServerSaveParams(void) {
    zfree(server.saveparams);
    server.saveparams = NULL;
    server.saveparamslen = 0;
}

20、加载 Redis 服务器的配置

loadServerConfig(configfile, options);

// 释放一个sds结构体变量所占用的内存空间。
sdsfree(options);

这段代码的作用是加载服务器配置文件并设置选项。
具体步骤如下:

  1. 函数传入两个参数:configfile和options。
  2. 加载完成之后,options就不需要了,就释放内存了

里面操作大致可以为

打开配置文件,读取其中的内容并拼接到config变量中,添加额外的配置选项,将config中的配置信息加载到服务器中,最后释放config变量的内存空间。

21、判断是否需要在后台启动 Redis 服务器

// 判断 Redis 服务器的运行模式是否为监管模式,并将结果赋值给 server.supervised。
server.supervised = redisIsSupervised(server.supervised_mode);
// 根据 Redis 服务器的配置和运行模式,判断是否需要在后台启动 Redis 服务器,并将结果赋值给 background。
int background = server.daemonize && !server.supervised;
// 如果需要在后台启动 Redis 服务器,则调用 daemonize() 函数进行后台启动。
if (background) daemonize();

这段代码的作用是判断是否需要在后台启动 Redis 服务器,如果需要,则执行后台启动操作。

22、服务端输出启动的信息

serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo");
serverLog(LL_WARNING,
          "Redis version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started",
          REDIS_VERSION,
          (sizeof(long) == 8) ? 64 : 32,
          redisGitSHA1(),
          strtol(redisGitDirty(), NULL, 10) > 0,
          (int) getpid());

if (argc == 1) {
    serverLog(LL_WARNING,
              "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf",
              argv[0], server.sentinel_mode ? "sentinel" : "redis");
} else {
    serverLog(LL_WARNING, "Configuration loaded");
}

23、读取当前进程的 OOM 分数调整值

readOOMScoreAdj();

Redis 在启动时会调用 readOOMScoreAdj() 函数读取当前进程的 OOM 分数调整值,并根据该值计算出 Redis 服务器进程的 OOM 分数,以便在内存紧张时进行适当的处理。

24、初始化服务器

initServer();

这个函数为服务器的正常运行打下了基础,后续的代码将会在此基础上进一步执行初始化和启动服务器的操作,包括:

  • 初始化服务器状态:包括各种计数器、标志位、配置值等。
  • 初始化服务器数据结构:包括数据库、字典、列表、集合、有序集合等。
  • 加载服务器配置:包括读取配置文件、解析配置参数等。
  • 初始化网络连接:包括创建套接字、绑定端口、监听连接等。
  • 注册事件处理器:包括注册文件事件、时间事件、信号事件等。
  • 启动服务器:进入事件循环,等待接收客户端连接并处理命令请求。
    总之,initServer函数是Redis服务器启动的关键函数,它负责初始化和准备服务器的各种资源和状态,为后续的命令处理和网络通信提供基础支持。

后续我会在详细的分析初始服务器的源码

25、创建PID文件&修改进程名称

  if (background || server.pidfile) createPidFile();

   redisSetProcTitle(argv[0]);

该代码首先创建一个PID文件,如果服务器在后台运行或者指定了PID文件。然后修改进程名称为传入的第一个参数。
具体步骤如下:

  1. 判断是否需要在后台运行或者指定了PID文件,如果需要,则调用createPidFile()函数创建PID文件
  2. 调用redisSetProcTitle()函数修改进程名称为传入的第一个参数。

26、打印艺术字体

redisAsciiArt();

redisAsciiArt()是Redis启动时打印的ASCII艺术字符画,类似于Logo。这个函数主要是为了让Redis启动时输出更加美观,增加用户体验。

27、检查TCP的backlog是否符合要求

checkTcpBacklogSettings();
void checkTcpBacklogSettings(void) {
#ifdef HAVE_PROC_SOMAXCONN
    FILE *fp = fopen("/proc/sys/net/core/somaxconn","r");
    char buf[1024];
    if (!fp) return;
    if (fgets(buf,sizeof(buf),fp) != NULL) {
        int somaxconn = atoi(buf);
        if (somaxconn > 0 && somaxconn < server.tcp_backlog) {
            serverLog(LL_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of %d.", server.tcp_backlog, somaxconn);
        }
    }
    fclose(fp);
#endif
}

这段代码的意义是检查TCP连接的backlog设置是否符合要求。backlog是指Linux内核中维护的一个队列,用于存放处于SYN_RCVD状态的TCP连接请求。当一个客户端向服务器发送连接请求时,如果服务器已经处于SYN_RCVD状态,那么服务器会将该连接请求放入backlog队列中,等待服务器处理。如果backlog队列已满,那么服务器就会忽略该连接请求,导致连接失败。

Redis在启动时会检查TCP连接的backlog设置是否符合要求,以保证服务器可以正确处理所有连接请求。如果backlog设置过小,那么可能会导致连接请求被忽略,从而影响Redis的性能和可靠性。如果backlog设置过大,那么可能会浪费服务器的资源,从而影响Redis的性能和扩展性。

因此,checkTcpBacklogSettings()函数的作用是检查TCP连接的backlog设置是否合理,如果不合理,则会输出警告信息。

这是为了避免在客户端连接数增加时可能发生的连接超时或拒绝连接等问题。建议在高流量场景下将backlog设置得较高

28、非哨兵模式

28.1 开启操作Linux、arm64检查

//  1. 判断是否在Linux系统上运行,如果是则执行下一步,否则结束。 
#ifdef __linux__ 
    linuxMemoryWarnings();
//  2. 判断是否是ARM64架构,如果是则执行下一步,否则结束。 
#if defined (__arm64__)
 	//  3. 调用linuxMadvFreeForkBugCheck()函数检查内核是否存在一个可能导致后台保存数据损坏的错误,并将返回值保存到ret变量中。 
    int ret;
    if ((ret = linuxMadvFreeForkBugCheck())) {
        // 如果检查成功,则判断返回值ret的值,如果是1,则输出警告信息并提示用户升级内核,否则输出错误信息并提示用户报告此错误。
        if (ret == 1)
            serverLog(LL_WARNING,"WARNING Your kernel has a bug that could lead to data corruption during background save. "
                                 "Please upgrade to the latest stable kernel.");
        else
            serverLog(LL_WARNING, "Failed to test the kernel for a bug that could lead to data corruption during background save. "
                                  "Your system could be affected, please report this error.");
        // 5、如果用户没有设置忽略警告,则输出警告信息并退出程序。 
        if (!checkIgnoreWarning("ARM64-COW-BUG")) {
            serverLog(LL_WARNING,"Redis will now exit to prevent data corruption. "
                                 "Note that it is possible to suppress this warning by setting the following config: ignore-warnings ARM64-COW-BUG");
            exit(1);
        }
    }
#endif /* __arm64__ */
#endif /* __linux__ */

这段代码是在Linux系统上运行的,如果是ARM64架构,则会检查内核是否存在一个可能导致后台保存数据损坏的错误。如果存在这个错误,则会输出警告信息并退出程序,如果用户设置了忽略警告,则程序不会退出。如果检测失败,则会输出错误信息并提示用户报告此错误。如果没有ARM64架构或者不是Linux系统,则不会执行任何操作。

28.2 加载模块

moduleLoadFromQueue();

redis 6.0中的moduleLoadFromQueue函数用于从Redis模块队列中加载模块。Redis模块是一种用C语言编写的插件,可以扩展Redis的功能。模块可以添加新的命令,数据类型和事件处理程序,从而使Redis更加灵活和可扩展。

在Redis服务器运行期间,如果需要加载新的模块或卸载现有的模块,可以通过发送MODULE LOAD或MODULE UNLOAD命令来实现。
这些命令会将指定的模块信息发送到队列中,然后由moduleLoadFromQueue()函数来处理。
如果加载或卸载成功,函数会返回相应的成功信息,否则会返回错误信息。

28.3 加载ACL用户列表

ACLLoadUsersAtStartup();

在Redis启动时从配置文件中加载ACL用户列表。ACL(Access Control List)是Redis提供的一种访问控制机制,可以限制客户端对Redis的访问权限。

ACL用户列表是一组定义在Redis配置文件中的用户和密码,用于授权客户端访问Redis。

ACLLoadUsersAtStartup()函数在Redis启动时被调用,它会读取Redis配置文件中的aclfile选项指定的文件,并解析其中的用户列表。如果文件不存在或解析失败,则函数返回REDIS_ERR,表示启动失败。如果解析成功,则将用户列表加载到Redis服务器的内存中,以便在后续的客户端连接中使用。

28.4 最后加载

  InitServerLast();

InitServerLast() 是 Redis 初始化的最后一个步骤,它完成了一些初始化工作

void InitServerLast() {
    // 用于初始化Redis服务器的后台I/O线程。 
    bioInit();
    // 用于初始化Redis服务器的多线程I/O模块。 
    initThreadedIO();
    // 用于设置jemalloc库的后台线程
    set_jemalloc_bg_thread(server.jemalloc_bg_thread);
    // 用于获取Redis服务器启动后的初始内存使用量。将这个值保存在server.initial_memory_usage变量中,以便后续使用。
    server.initial_memory_usage = zmalloc_used_memory();
}

28.5 读取Redis持久化数据到内存之中

 loadDataFromDisk();

这段函数的作用是从磁盘中读取Redis持久化的数据到内存中,用于在服务器启动时恢复数据。具体来说,它会执行以下操作:

  1. 读取RDB文件或者AOF文件中的数据到内存中。
  2. 根据读取的数据构建相应的数据结构,如字符串、哈希表、列表等。
  3. 将构建好的数据结构保存在内存中,供Redis服务器在运行时使用。
    这个函数通常在服务器启动时被调用,它是Redis持久化机制的一部分,用于保证Redis服务器在重启后能够恢复之前的数据状态。

28.6 集群模式检查DB

if (server.cluster_enabled) {
    if (verifyClusterConfigWithData() == C_ERR) {
        serverLog(LL_WARNING,
                  "You can't have keys in a DB different than DB 0 when in "
                  "Cluster mode. Exiting.");
        exit(1);
    }
}

集群模式检查0号数据库,在 Redis 集群模式下,每个节点只维护数据库0,

因此如果在其他数据库中有数据,则无法将该节点添加到集群中。如果发现存在非 0 号数据库中存在数据,程序将输出错误信息并退出

28.7 来连接把!

if (server.ipfd_count > 0 || server.tlsfd_count > 0)
    serverLog(LL_NOTICE, "Ready to accept connections");

这段代码是在Redis启动成功后,当IP套接字或TLS套接字的数量大于0时,打印一个日志记录,表示Redis已经准备好接受客户端连接了。

if (server.sofd > 0)
    serverLog(LL_NOTICE, "The server is now ready to accept connections at %s", server.unixsocket);

这段代码的作用是在Redis启动成功后,如果Redis实例绑定了TCP端口或TLS端口,或者绑定了Unix socket,就会在日志中打印出相应的提示信息,表明Redis已经准备好接收连接

if (server.supervised_mode == SUPERVISED_SYSTEMD) {
    if (!server.masterhost) {
        redisCommunicateSystemd("STATUS=Ready to accept connections\n");
        redisCommunicateSystemd("READY=1\n");
    } else {
        redisCommunicateSystemd("STATUS=Waiting for MASTER <-> REPLICA sync\n");
    }
}

这段代码的作用是,如果Redis服务器处于systemd监管模式下,会向systemd发送一些状态信息。

如果该Redis服务器是主服务器,则发送“Waiting for MASTER <-> REPLICA sync”信息;

如果是从服务器,则发送“Ready to accept connections”和“READY=1”信息。

29、哨兵模式

InitServerLast();
    sentinelIsRunning();
    if (server.supervised_mode == SUPERVISED_SYSTEMD) {
        redisCommunicateSystemd("STATUS=Ready to accept connections\n");
        redisCommunicateSystemd("READY=1\n");
}

同28.4 加载服务最后的配置

然后检查sentinel是否在运行(检查redis的配置文件,配置文件是否可写,增加sentinel的监控等等)

如果服务器在 systemd 监管模式下运行,则服务器也会通过 systemd 的 API 与其通信以通知其已准备好接受连接。

30、设置CPU的亲和性

 redisSetCpuAffinity(server.server_cpulist);

redisSetCpuAffinity是Redis中的一个函数,用于设置Redis进程的CPU亲和性,即指定进程在哪些CPU上运行。

在调用该函数之前,Redis会从配置文件中读取server_cpulist参数,该参数的值是一个以逗号分隔的CPU核心列表,例如"0,1,2,3"。如果该参数为空,则表示不设置CPU亲和性,Redis进程可以在任何CPU上运行。

在Linux系统中,CPU亲和性可以通过sched_setaffinity系统调用来实现。Redis通过该系统调用将进程绑定到指定的CPU核心。具体来说,redisSetCpuAffinity函数会先检查当前系统是否支持sched_setaffinity系统调用,如果支持则根据server_cpulist参数的值调用sched_setaffinity函数,将Redis进程绑定到指定的CPU核心上。如果不支持,则该函数不执行任何操作。

设置CPU亲和性可以提高Redis的性能,因为它可以避免进程在不同的CPU核心之间频繁切换,从而减少上下文切换的开销。另外,设置CPU亲和性还可以避免CPU缓存的失效,从而进一步提高Redis的性能。

31、提升内存不足时的稳定性

setOOMScoreAdj(-1);

setOOMScoreAdj(-1)是一个Linux特有的系统调用,它的作用是将当前进程的OOM killer值设置为最低值,从而降低进程被系统强制杀死的概率。在Redis中,这个函数通常用于降低Redis进程被系统强制杀死的概率,从而提高Redis的可靠性和稳定性。

32、启动事件处理

aeMain(server.el);

监听事件 Redis事件循环
aeMain()函数会阻塞当前线程,直到有事件就绪,然后调用相应的事件处理函数。在事件处理完毕后,aeMain()函数会继续循环处理其他事件。
这个函数在Redis启动后一直运行,直到Redis进程被关闭。

33、END

aeDeleteEventLoop(server.el);

这段代码的作用是删除Redis服务器的事件循环。

void aeDeleteEventLoop(aeEventLoop *eventLoop) {
    aeApiFree(eventLoop);
    zfree(eventLoop->events);
    zfree(eventLoop->fired);

    /* Free the time events list. */
    aeTimeEvent *next_te, *te = eventLoop->timeEventHead;
    while (te) {
        next_te = te->next;
        zfree(te);
        te = next_te;
    }
    zfree(eventLoop);
}

Redis使用事件驱动的方式处理客户端请求和内部任务,其中事件循环是Redis事件驱动模型的核心。

当Redis服务器启动时,它会创建一个事件循环,并将其注册到系统中。事件循环会不断地监听和处理各种事件,如客户端请求、定时器事件、文件事件等。

当事件发生时,事件循环会调用相应的事件处理函数进行处理。

在某些情况下,需要删除Redis的事件循环,如服务器关闭时或者发生错误时。此时,调用aeDeleteEventLoop函数可以将事件循环从系统中删除,确保Redis服务器正常退出或重启。

34、总结

redis架构设计: redis-server的启动(硬核分析)_第1张图片

Redis server的主方法主要包含以下几个核心步骤:

  1. 解析命令行参数:Redis server启动时,会解析命令行参数,包括端口号、配置文件路径等参数。
  2. 初始化服务器:Redis server会进行一系列的初始化操作,包括加载配置文件、创建事件循环、初始化数据库等。
  3. 运行事件循环:Redis server的核心功能是事件驱动的,它会不断地运行事件循环,监听和处理各种事件,如客户端请求、定时器事件、文件事件等。

后面呢,我会详细的写redis-server核心的步骤还有相关的架构设计。希望大家继续关注哦。

你可能感兴趣的:(redis,缓存,数据库)