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