|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
这篇文章主要从 server.c 的 main 函数开始,来一步步拆解 Redis 的启动流程。
先看 main 函数的全貌
在开始详细了解启动流程之前呢,我们可以从main函数入手,大体上看看这个方法中干了哪些事情,
main 函数的核心流程可以概括为:
初始化基础库 -> 判断Sentinel模式 -> 初始化默认配置 -> 加载配置 -> 守护进程化 -> 初始化服务器 -> 加载数据 -> 进入事件循环
代码的主体骨架是下面这样的,其中去掉了一些错误处理、日志输出、条件编译等代码,只保留关键流程~
int main(int argc, char **argv) {
// ==================== 1. 基础初始化 ====================
setlocale(LC_COLLATE,""); // 设置本地化环境,影响字符串比较
tzset(); // 初始化时区信息,填充timezone全局变量
zmalloc_set_oom_handler(redisOutOfMemoryHandler); // 设置内存分配失败的回调函数
srand(time(NULL)^getpid()); // 初始化随机数种子,用时间+进程ID增加随机性
gettimeofday(&tv,NULL); // 获取当前时间,用于后续计时
char hashseed[16]; // 生成16字节的哈希种子
getRandomHexChars(hashseed,sizeof(hashseed)); // 随机生成十六进制字符串
dictSetHashFunctionSeed((uint8_t*)hashseed); // 设置字典哈希函数种子,防止哈希攻击
// ==================== 2. 判断是否 Sentinel 模式 ====================
server.sentinel_mode = checkForSentinelMode(argc,argv); // 检查是否以Sentinel模式启动
// ==================== 3. 初始化默认配置 ====================
initServerConfig(); // 初始化server结构体的默认配置值
moduleInitModulesSystem(); // 初始化模块子系统
// 保存可执行文件路径和命令行参数,用于后续重启
server.executable = getAbsolutePath(argv[0]); // 获取绝对路径
server.exec_argv = zmalloc(sizeof(char*)*(argc+1)); // 分配参数数组
for (j = 0; j < argc; j++)
server.exec_argv[j] = zstrdup(argv[j]); // 复制每个参数
// ==================== 4. Sentinel 模式特殊初始化 ====================
if (server.sentinel_mode) {
initSentinelConfig(); // Sentinel专用配置
initSentinel(); // 初始化Sentinel数据结构
}
// ==================== 5. 加载配置 ====================
resetServerSaveParams(); // 重置RDB save参数为空
loadServerConfig(configfile, options); // 加载配置文件和命令行参数
// ==================== 6. 守护进程化 ====================
server.supervised = redisIsSupervised(server.supervised_mode); // 检测是否被 supervisor 管理
int background = server.daemonize && !server.supervised; // 判断是否后台运行
if (background)
daemonize(); // fork 子进程并脱离终端
// ==================== 7. 初始化服务器 ====================
initServer(); // 核心!创建事件循环、监听端口、初始化数据库等
if (background || server.pidfile)
createPidFile(); // 创建pid文件
redisSetProcTitle(argv[0]); // 设置进程标题,方便ps查看
redisAsciiArt(); // 打印Redis ASCII art logo
checkTcpBacklogSettings(); // 检查系统TCP backlog设置,打印警告
// ==================== 8. 加载数据 ====================
if (!server.sentinel_mode) { // Sentinel模式不加载数据
serverLog(LL_WARNING,"Server initialized");
moduleLoadFromQueue(); // 加载配置中指定的模块
loadDataFromDisk(); // 加载RDB或AOF文件
} else {
sentinelIsRunning(); // Sentinel启动完成回调
}
// ==================== 9. 进入事件循环 ====================
aeSetBeforeSleepProc(server.el, beforeSleep); // 设置每轮循环前的回调
aeSetAfterSleepProc(server.el, afterSleep); // 设置每轮循环后的回调
aeMain(server.el); // 进入主事件循环,开始处理请求
aeDeleteEventLoop(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。
int checkForSentinelMode(int argc, char **argv) {
int j;
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
这个判断必须在 initServerConfig() 之前完成,因为后续的初始化流程会根据 server.sentinel_mode 走不同分支:
第三步:initServerConfig - 初始化默认配置
这个函数大约 200 行,主要就是给 server 这个全局结构体的各个字段赋默认值。
这些默认值可以通过 redis.conf 配置文件或命令行参数覆盖。采用"先设默认值,再加载配置"的方式,必须确保所有字段都有确定的初始值。
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);
updateCachedTime(); // 更新缓存时间
// 生成运行 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();
命令表是一个字典(dict),key 是命令名(如 “get”、“set”),value 是 redisCommand 结构体指针。当客户端发送 SET key value 时,Redis 会:
- 解析命令名 “set”
- 在
server.commands字典中查找对应的redisCommand - 检查参数个数、权限、标志等
- 调用
redisCommand.proc指向的函数(这里是setCommand)
客户端命令 "SET foo bar"
↓
解析命令名 "set"
↓
dictFind(server.commands, "set")
↓
获取 redisCommand 结构体
↓
检查 arity=-3(至少 3 个参数)✓
检查 flags 中 CMD_WRITE 标志 ✓
↓
调用 setCommand(client)
↓
返回 "+OK\r\n"
这种设计让 Redis 的命令处理非常灵活:
- 新增命令只需在
redisCommandTable[]加一行 rename-command可以修改命令名或禁用命令- 命令标志系统控制着权限、复制、脚本执行等行为
之后调用 moduleInitModulesSystem() 初始化模块系统。
第四步:Sentinel 模式特殊初始化
如果是 Sentinel 模式,需要提前初始化 Sentinel 相关数据结构,因为后续加载配置文件时会往里面填充要监控的主节点信息:
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
第五步:加载配置
配置来源有两个:配置文件和命令行参数。
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引入其他配置文件。
第六步:守护进程化
首先检测是否被 supervisor 管理:
server.supervised = redisIsSupervised(server.supervised_mode);
int background = server.daemonize && !server.supervised;
if (background) daemonize();
如果配置了 daemonize yes 且不在supervisor模式下,Redis会调用 daemonize() 函数:
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 - 真正的服务器初始化
这是最核心的初始化函数,干了下面这些事:
7.1 信号处理
signal(SIGHUP, SIG_IGN); // 忽略终端挂起
signal(SIGPIPE, SIG_IGN); // 忽略管道破裂
setupSignalHandlers(); // 注册SIGINT、SIGTERM等信号处理
7.2 创建共享对象
createSharedObjects();
创建一些常用的小整数、常用字符串(如OK、ERR、命令回复等)的共享对象,避免重复分配。
7.3 调整文件描述符限制
adjustOpenFilesLimit();
根据 maxclients 配置调整进程能打开的最大文件描述符数量。
7.4 创建事件循环和各种链表
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING, "Failed creating the event loop...");
exit(1);
}
server.clients = listCreate(); // 客户端列表
server.clients_index = raxNew(); // 客户端索引(rax树)
server.clients_to_close = listCreate(); // 待关闭客户端
server.slaves = listCreate(); // 从节点列表
server.monitors = listCreate(); // monitor客户端
server.clients_pending_write = listCreate();// 待写回客户端
server.unblocked_clients = listCreate(); // 已取消阻塞客户端
server.ready_keys = listCreate(); // 就绪的key
server.clients_waiting_acks = listCreate(); // 等待ACK的客户端
CONFIG_FDSET_INCR 是个冗余值,确保 fd 数量够用。
7.5 监听端口
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) {
unlink(server.unixsocket);
server.sofd = anetUnixServer(server.neterr, server.unixsocket,
server.unixsocketperm, server.tcp_backlog);
if (server.sofd == ANET_ERR) {
serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
exit(1);
}
anetNonBlock(NULL, server.sofd);
}
如果没有监听任何端口或socket,直接退出:
if (server.ipfd_count == 0 && server.sofd < 0) {
serverLog(LL_WARNING, "Configured to not listen anywhere, exiting.");
exit(1);
}
7.6 初始化数据库
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;
server.db[j].avg_ttl = 0;
server.db[j].defrag_later = listCreate();
}
每个 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字典。
7.7 注册时间事件 - 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 后首次触发,之后根据返回值决定下次触发间隔。
关于这个方法我们会在后面的文章中专门分析,敬请期待~
7.8 注册文件事件 - 接受连接
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.");
}
}
// Unix socket也注册
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR)
serverPanic("Unrecoverable error creating server.sofd file event.");
把监听socket的读事件注册到事件循环,回调是 acceptTcpHandler 或 acceptUnixHandler。有新连接时触发,accept后创建client结构体。
7.9 打开 AOF 文件
if (server.aof_state == AOF_ON) {
server.aof_fd = open(server.aof_filename,
O_WRONLY|O_APPEND|O_CREAT,0644);
if (server.aof_fd == -1) {
serverLog(LL_WARNING, "Can't open the append-only file: %s",
strerror(errno));
exit(1);
}
}
7.10 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); // 3 GB
server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
32位进程地址空间只有4GB,不限制的话容易OOM。Redis自动设置3GB限制。
7.11 初始化其他模块
if (server.cluster_enabled) clusterInit(); // 集群
replicationScriptCacheInit(); // 复制脚本缓存
scriptingInit(1); // Lua脚本
slowlogInit(); // 慢查询日志
latencyMonitorInit(); // 延迟监控
bioInit(); // 后台IO线程
server.initial_memory_usage = zmalloc_used_memory();
第八步:启动后的收尾工作
if (background || server.pidfile) createPidFile(); // 创建pid文件
redisSetProcTitle(argv[0]); // 设置进程标题
redisAsciiArt(); // 打印ASCII art
checkTcpBacklogSettings(); // 检查TCP backlog设置
进程标题
redisSetProcTitle(argv[0]);
设置进程标题,ps 能看到 “redis-server *:6379” 这样的名字,方便排查。
ASCII Art Logo
redisAsciiArt();
启动时打印那个Redis的ASCII art logo。
第九步:loadDataFromDisk - 加载数据
数据加载只在非 Sentinel 模式下执行, 因为Sentinel 不存储业务数据,
它只维护自己的状态信息(监控的主节点列表、其他 Sentinel 节点、failover 状态等)。这些状态信息存在 sentinel.masters 字典中,而不是 Redis 的数据库(server.db[])。
if (!server.sentinel_mode) {
serverLog(LL_WARNING,"Server initialized");
moduleLoadFromQueue(); // 加载模块
loadDataFromDisk(); // 加载数据
} else {
sentinelIsRunning();
}
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会打印进度日志。
如果是集群模式,还会验证数据是否都在DB 0:
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);
}
}
第十步:进入事件循环
aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
beforeSleep 在每轮事件循环开始前执行,主要做:
- 处理待写回的客户端数据
- 快速处理一些过期 key
- 解除阻塞客户端
afterSleep 在事件循环唤醒后执行。
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开始接受连接,处理请求。
启动流程梳理

|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
&spm=1001.2101.3001.5002&articleId=159737741&d=1&t=3&u=5ed16e917f124a438c761e80bf9be88e)
1208

被折叠的 条评论
为什么被折叠?



