Redis 源码简洁剖析 07 - main 函数启动

Posted by Yano on February 5, 2022

前言

main 函数是 Redis 整个运行程序的入口。源码主要在 server.c 文件中。

前面 6 篇文章分析了 Redis 的基础数据结构。

问题

  • Redis server 启动后具体会做哪些初始化操作?
  • Redis server 初始化时有哪些关键配置项?
  • Redis server 如何开始处理客户端请求?

阶段 1:基本初始化

基本的初始化工作,包括设置 server 运行的时区等。

//设置时区
setlocale(LC_COLLATE,"");
tzset();
...
//设置随机种子
char hashseed[16];
getRandomHexChars(hashseed,sizeof(hashseed));
dictSetHashFunctionSeed((uint8_t*)hashseed);

阶段 2:检查哨兵模式,执行 RDB 或 AOF 检测

Redis Server 可能以哨兵模式运行。哨兵模式需要额外的参数配置及初始化。

// 判断 server 是否为「哨兵模式」
if (server.sentinel_mode) {
    // 初始化哨兵配置
    initSentinelConfig();
    // 初始化哨兵模式
    initSentinel();
}

此外还会检查是否要执行 RDB 检测或 AOF 检查,这对应了实际运行的程序是 redis-check-rdb 或 redis-check-aof。

// 运行的是 redis-check-rdb
if (strstr(argv[0],"redis-check-rdb") != NULL)
    // 检测 RDB 文件
    redis_check_rdb_main(argc,argv,NULL);
    // 运行的是 redis-check-aof
else if (strstr(argv[0],"redis-check-aof") != NULL)
    // 检测 AOF 文件
    redis_check_aof_main(argc,argv);

阶段 3:运行参数解析

main 函数会对命令行传入的参数进行解析,并且调用 loadServerConfig 函数,对命令行参数和配置文件中的参数进行合并处理,然后为 Redis 各功能模块的关键参数设置合适的取值。

int main(int argc, char **argv) {
    
    //保存命令行参数
    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
    
    if (argc >= 2) {
    
        //对每个运行时参数进行解析
        while(j != argc) {
            
        }
    
    loadServerConfig(configfile,options);
}

loadServerConfig 函数是在 config.c 文件中实现的,该函数是以 Redis 配置文件和命令行参数的解析字符串为参数,将配置文件中的所有配置项读取出来,形成字符串。

阶段 4:初始化 server

调用 initServer 函数,对 server 运行时的各种资源进行初始化工作。这主要包括:

  • server 资源管理所需的数据结构初始化
  • 键值对数据库初始化
  • server 网络框架初始化

接着会再次判断是否为「哨兵模式」:

  • 是哨兵模式,调用 sentinelIsRunning 函数,设置启动哨兵模式
  • 不是哨兵模式,调用 loadDataFromDisk 函数,从磁盘加载 AOF 或 RDB 文件,恢复之前的数据
// 初始化 server
initServer();
……

if (!server.sentinel_mode) {
    ……
    InitServerLast();
    // 从磁盘加载数据
    loadDataFromDisk();
    ……
} else {
    ……
    sentinelIsRunning();
    ……
}

资源管理

和 server 连接的客户端、从库等,Redis 用作缓存时的替换候选集,以及 server 运行时的状态信息,这些资源的管理信息都会在 initServer 函数中进行初始化。

初始化数据库

因为一个 Redis 实例可以同时运行多个数据库,所以 initServer 函数会使用一个循环,依次为每个数据库创建相应的数据结构。

for (j = 0; j < server.dbnum; j++) {
    // 创建全局哈希表
    server.db[j].dict = dictCreate(&dbDictType,NULL);
    // 创建过期 key 的信息表
    server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
    server.db[j].expires_cursor = 0;
    // 为被 BLPOP 阻塞的 key 创建信息表
    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
    // 为将执行 PUSH 的阻塞 key 创建信息表
    server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
    // 为被 MULTI/WATCH 操作监听的 key 创建信息表
    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
    ……
}

创建事件驱动框架

针对每个监听 IP 上可能发生的客户端连接,都创建了监听事件,用来监听客户端连接请求。同时,initServer 为监听事件设置了相应的处理函数 acceptTcpHandler。

这样一来,只要有客户端连接到 server 监听的 IP 和端口,事件驱动框架就会检测到有连接事件发生,然后调用 acceptTcpHandler 函数来处理具体的连接。

//创建事件循环框架
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);

//开始监听设置的网络端口
if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
        exit(1);

//为 server 后台任务创建定时事件
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
}

阶段 5:执行事件驱动框架

高效处理高并发的客户端连接请求,Redis 采用了事件驱动框架,来并发处理不同客户端的连接和读写请求。main 函数最后会调用 aeMain 函数进入事件驱动框架,循环处理各种触发的事件。

// 事件驱动框架,循环处理各种触发的事件
aeMain(server.el);
// 循环结束,删除 eventLoop
aeDeleteEventLoop(server.el);

aeMain 函数核心调用了 aeProcessEvents 函数。aeProcessEvents 函数的具体源码将在之后的文章中分析。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    // 循环调用
    while (!eventLoop->stop) {
        // 核心函数,处理事件的逻辑
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

参考链接

  • [极客时间:08 Redis server 启动后会做哪些操作?](https://time.geekbang.org/column/article/406556)

Redis 源码简洁剖析系列

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

我的公众号

coding 笔记、读书笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注^_^