Redis 启动流程全解析(server.c 到 main 函数)
启动一个 Redis 实例看起来很简单,redis-server 一敲就完了。但你有没有想过,从按下回车到 Redis 开始接受连接,中间发生了什么?
这篇文章从 server.c 的 main 函数开始,一步步拆解 Redis 的启动流程。
先看 main 函数的全貌
main 函数在 server.c 的第 4000 行附近,核心流程可以概括为:
1初始化基础库 → 加载配置 → 初始化服务器 → 加载数据 → 进入事件循环
代码骨架是这样的:
1int main(int argc, char **argv) {
2 // 1. 基础初始化
3 initServerConfig();
4 moduleInitModulesSystem();
5
6 // Sentinel 模式初始化(如果是)
7 if (server.sentinel_mode) {
8 initSentinelConfig();
9 initSentinel();
10 }
11
12 // 2. 加载配置文件和命令行参数
13 resetServerSaveParams();
14 loadServerConfig(configfile, options);
15
16 // 3. 以守护进程方式运行(如果配置了)
17 if (background) daemonize();
18
19 // 4. 初始化服务器
20 initServer();
21
22 // 5. 从磁盘加载数据
23 loadDataFromDisk();
24
25 // 6. 进入事件循环
26 aeMain(server.el);
27
28 return 0;
29}
下面逐个展开。
第一步:基础初始化
main 函数开头做了一些必须先做的初始化:
1setlocale(LC_COLLATE,"");
2tzset(); // 时区初始化
3zmalloc_set_oom_handler(redisOutOfMemoryHandler); // 内存不足处理
4srand(time(NULL)^getpid()); // 随机种子
5gettimeofday(&tv,NULL);
6
7// 生成哈希种子,用于字典的哈希函数
8char hashseed[16];
9getRandomHexChars(hashseed,sizeof(hashseed));
10dictSetHashFunctionSeed((uint8_t*)hashseed);
这些操作和 Redis 业务逻辑无关,但是基础库需要的。比如哈希种子,每次启动都不一样,防止哈希碰撞攻击。
然后判断是否是 Sentinel 模式:
1server.sentinel_mode = checkForSentinelMode(argc,argv);
判断方式很简单:看可执行文件名是不是 redis-sentinel,或者参数里有没有 --sentinel。
第二步:initServerConfig - 初始化默认配置
这个函数很长,大概 500 行,主要就是给 server 这个全局结构体的各个字段赋默认值。
1void initServerConfig(void) {
2 // 互斥锁初始化
3 pthread_mutex_init(&server.next_client_id_mutex,NULL);
4 pthread_mutex_init(&server.lruclock_mutex,NULL);
5 pthread_mutex_init(&server.unixtime_mutex,NULL);
6
7 // 生成运行 ID
8 getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
9 server.runid[CONFIG_RUN_ID_SIZE] = '\0';
10
11 // 基础配置
12 server.port = CONFIG_DEFAULT_SERVER_PORT; // 6379
13 server.dbnum = CONFIG_DEFAULT_DBNUM; // 16
14 server.verbosity = CONFIG_DEFAULT_VERBOSITY;
15 server.maxidletime = CONFIG_DEFAULT_CLIENT_TIMEOUT;
16 server.tcpkeepalive = CONFIG_DEFAULT_TCP_KEEPALIVE;
17
18 // AOF 相关
19 server.aof_state = AOF_OFF;
20 server.aof_fsync = CONFIG_DEFAULT_AOF_FSYNC;
21
22 // RDB 相关
23 server.rdb_filename = zstrdup(CONFIG_DEFAULT_RDB_FILENAME);
24 server.rdb_compression = CONFIG_DEFAULT_RDB_COMPRESSION;
25
26 // 内存相关
27 server.maxmemory = CONFIG_DEFAULT_MAXMEMORY;
28 server.maxmemory_policy = CONFIG_DEFAULT_MAXMEMORY_POLICY;
29
30 // ... 还有很多
31}
有个有意思的地方——RDB 默认的 save 策略:
1resetServerSaveParams();
2appendServerSaveParams(60*60,1); // 1小时内有1次修改就save
3appendServerSaveParams(300,100); // 5分钟内有100次修改
4appendServerSaveParams(60,10000); // 1分钟内有10000次修改
还有命令表的初始化:
1server.commands = dictCreate(&commandTableDictType,NULL);
2server.orig_commands = dictCreate(&commandTableDictType,NULL);
3populateCommandTable();
populateCommandTable 把所有命令注册到字典里,支持 rename-command 配置项重命名命令。
第三步:加载配置
配置来源有两个:配置文件和命令行参数。
1if (argc >= 2) {
2 j = 1;
3 sds options = sdsempty();
4 char *configfile = NULL;
5
6 // 第一个参数如果不是 --开头,就当配置文件路径
7 if (argv[j][0] != '-' || argv[j][1] != '-') {
8 configfile = argv[j];
9 server.configfile = getAbsolutePath(configfile);
10 j++;
11 }
12
13 // 剩下的参数转成配置字符串
14 // 比如 --port 6380 转成 "port 6380\n"
15 while(j != argc) {
16 if (argv[j][0] == '-' && argv[j][1] == '-') {
17 if (sdslen(options)) options = sdscat(options,"\n");
18 options = sdscat(options,argv[j]+2);
19 options = sdscat(options," ");
20 } else {
21 options = sdscatrepr(options,argv[j],strlen(argv[j]));
22 options = sdscat(options," ");
23 }
24 j++;
25 }
26
27 // 加载配置
28 loadServerConfig(configfile, options);
29}
这样设计的好处是配置可以灵活组合:
1redis-server /etc/redis.conf --port 6380 --maxmemory 1gb
配置文件里的设置会被命令行参数覆盖。
loadServerConfig 函数做的就是逐行解析配置,设置到 server 结构体里。支持 INCLUDE 引入其他配置文件。
第四步:守护进程化
如果配置了 daemonize yes,Redis 会调用 daemonize() 函数:
1int background = server.daemonize && !server.supervised;
2if (background) daemonize();
daemonize() 的实现是经典的 Unix 守护进程创建流程:
1void daemonize(void) {
2 int fd;
3
4 if (fork() != 0) exit(0); // 父进程退出
5 setsid(); // 创建新会话
6
7 if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
8 dup2(fd, STDIN_FILENO);
9 dup2(fd, STDOUT_FILENO);
10 dup2(fd, STDERR_FILENO);
11 if (fd > STDERR_FILENO) close(fd);
12 }
13}
fork 后父进程退出,子进程脱离终端,重定向标准输入输出到 /dev/null。
第五步:initServer - 真正的服务器初始化
这是最核心的初始化函数,干了这些事:
5.1 信号处理
1signal(SIGHUP, SIG_IGN); // 忽略终端挂起
2signal(SIGPIPE, SIG_IGN); // 忽略管道破裂
3setupSignalHandlers(); // 注册 SIGINT、SIGTERM 等信号处理
5.2 创建各种链表和数据结构
1server.clients = listCreate(); // 客户端列表
2server.clients_to_close = listCreate(); // 待关闭客户端
3server.slaves = listCreate(); // 从节点列表
4server.monitors = listCreate(); // monitor 客户端
5server.clients_pending_write = listCreate();// 待写回客户端
6server.unblocked_clients = listCreate(); // 已取消阻塞客户端
5.3 创建事件循环
1server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
2if (server.el == NULL) {
3 serverLog(LL_WARNING, "Failed creating the event loop...");
4 exit(1);
5}
CONFIG_FDSET_INCR 是个冗余值,确保 fd 数量够用。
5.4 监听端口
1if (server.port != 0 &&
2 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
3 exit(1);
listenToPort 创建 socket,bind,listen,把 fd 存到 server.ipfd 数组。
如果配置了 Unix socket:
1if (server.unixsocket != NULL) {
2 server.sofd = anetUnixServer(server.neterr, server.unixsocket,
3 server.unixsocketperm, server.tcp_backlog);
4}
5.5 初始化数据库
Redis 默认创建 16 个数据库(由 dbnum 配置),通过 SELECT n 命令切换。
1server.db = zmalloc(sizeof(redisDb)*server.dbnum);
2
3for (j = 0; j < server.dbnum; j++) {
4 server.db[j].dict = dictCreate(&dbDictType,NULL); // 数据字典
5 server.db[j].expires = dictCreate(&keyptrDictType,NULL); // 过期时间字典
6 server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
7 server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
8 server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
9 server.db[j].id = j;
10}
每个 redisDb 结构体包含多个字典,各司其职:
| 字典 | 用途 |
|---|---|
dict |
存储所有键值对,核心数据结构 |
expires |
存储键的过期时间(指针指向 dict 中的 key) |
blocking_keys |
存储 BLPOP 等命令阻塞等待的 key 及对应客户端 |
ready_keys |
LPUSH/RPUSH 后唤醒阻塞客户端的待处理 key |
watched_keys |
MULTI/EXEC 事务中 WATCH 监视的 key |
dict 和 expires 分开存储的设计很巧妙:不设置过期时间的 key 不需要在 expires 中占空间,节省内存。过期检查时只需遍历 expires 字典。
5.6 注册时间事件 - serverCron
1if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
2 serverPanic("Can't create event loop timers.");
3 exit(1);
4}
serverCron 是 Redis 的定时任务中心,负责:
- 清理过期 key
- 更新 LRU 时钟
- 处理 BGSAVE 和 AOF 重写
- 主从复制心跳
- 内存统计
- 等等
1ms 后首次触发,之后根据返回值决定下次触发间隔。
5.7 注册文件事件 - 接受连接
1for (j = 0; j < server.ipfd_count; j++) {
2 if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
3 acceptTcpHandler,NULL) == AE_ERR)
4 {
5 serverPanic("Unrecoverable error creating server.ipfd file event.");
6 }
7}
把监听 socket 的读事件注册到事件循环,回调是 acceptTcpHandler。有新连接时触发,accept 后创建 client 结构体。
5.8 初始化其他模块
1if (server.cluster_enabled) clusterInit(); // 集群
2replicationScriptCacheInit(); // 复制脚本缓存
3scriptingInit(1); // Lua 脚本
4slowlogInit(); // 慢查询日志
5latencyMonitorInit(); // 延迟监控
6bioInit(); // 后台 IO 线程
第六步:loadDataFromDisk - 加载数据
1void loadDataFromDisk(void) {
2 long long start = ustime();
3
4 if (server.aof_state == AOF_ON) {
5 // AOF 模式,加载 AOF 文件
6 if (loadAppendOnlyFile(server.aof_filename) == C_OK)
7 serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",
8 (float)(ustime()-start)/1000000);
9 } else {
10 // RDB 模式,加载 RDB 文件
11 rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
12 if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
13 serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
14 (float)(ustime()-start)/1000000);
15 }
16 }
17}
如果 AOF 开了,优先加载 AOF,因为 AOF 数据更完整。否则加载 RDB。
加载数据可能很慢,取决于数据量和磁盘速度。期间 Redis 会打印进度日志。
第七步:进入事件循环
1aeSetBeforeSleepProc(server.el, beforeSleep);
2aeSetAfterSleepProc(server.el, afterSleep);
3aeMain(server.el);
4aeDeleteEventLoop(server.el);
beforeSleep 在每轮事件循环开始前执行,主要做:
- 处理待写回的客户端数据
- 快速处理一些过期 key
- 解除阻塞客户端
aeMain 就是那个死循环:
1void aeMain(aeEventLoop *eventLoop) {
2 eventLoop->stop = 0;
3 while (!eventLoop->stop) {
4 if (eventLoop->beforesleep != NULL)
5 eventLoop->beforesleep(eventLoop);
6 aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
7 }
8}
至此,Redis 开始接受连接,处理请求。
启动流程梳理
1main()
2 │
3 ├─ 基础初始化(时区、随机种子、哈希种子)
4 │
5 ├─ initServerConfig() ← 默认配置
6 │
7 ├─ loadServerConfig() ← 加载配置文件和命令行参数
8 │
9 ├─ daemonize() ← 守护进程化(可选)
10 │
11 ├─ initServer() ← 核心!
12 │ ├─ 信号处理
13 │ ├─ 创建事件循环
14 │ ├─ 监听端口
15 │ ├─ 初始化数据库
16 │ ├─ 注册 serverCron 时间事件
17 │ ├─ 注册 acceptTcpHandler 文件事件
18 │ └─ 初始化集群、Lua、慢日志等模块
19 │
20 ├─ loadDataFromDisk() ← 加载 RDB/AOF
21 │
22 └─ aeMain() ← 进入事件循环,开始服务
一些有意思的细节
32 位实例的内存限制
1if (server.arch_bits == 32 && server.maxmemory == 0) {
2 serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
3 server.maxmemory = 3072LL*(1024*1024);
4 server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
5}
32 位进程地址空间只有 4GB,不限制的话容易 OOM。Redis 自动设置 3GB 限制。
进程标题
1redisSetProcTitle(argv[0]);
设置进程标题,ps 能看到 “redis-server *:6379” 这样的名字,方便排查。
ASCII Art Logo
1redisAsciiArt();
启动时打印那个 Redis 的 ASCII art logo。