Redis 配置加载机制源码分析
Redis 配置加载机制源码分析
redis.conf 里的那些配置项,Redis 是怎么读到内存里去的?运行时用 CONFIG SET 改了配置,重启后还能生效吗?这篇文章来拆解配置加载的完整流程。
一、配置加载入口
启动时,main() 函数调用 loadServerConfig():
void loadServerConfig(char *filename, char *options) { sds config = sdsempty(); // 创建空的 sds 字符串,用于存放配置内容 char buf[CONFIG_MAX_LINE+1]; // 行缓冲区,CONFIG_MAX_LINE 是 1024 if (filename) { // filename 可能为 NULL(只用命令行参数时) FILE *fp; if (filename[0] == '-' && filename[1] == '\0') { // "redis-server -" 表示从 stdin 读配置 fp = stdin; } else { if ((fp = fopen(filename,"r")) == NULL) { serverLog(LL_WARNING, "Fatal error, can't open config file '%s'", filename); exit(1); } } // 逐行读取文件内容,拼接到 config 字符串 while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL) config = sdscat(config,buf); // sdscat 会自动扩容 if (fp != stdin) fclose(fp); // stdin 不能 fclose } // 把命令行参数追加到配置字符串末尾 // 这样命令行参数就能覆盖配置文件里的同名配置 if (options) { config = sdscat(config,"\n"); // 先加个换行,和前面隔开 config = sdscat(config,options); // options 已经是 "key value" 格式 } loadServerConfigFromString(config); sdsfree(config); // 释放临时字符串}
逻辑很简单:把配置文件读到内存,再拼上命令行传入的 options,最后调 loadServerConfigFromString() 解析。
二、逐行解析配置
loadServerConfigFromString() 才是真正干活的地方,核心是一个大循环:
void loadServerConfigFromString(char *config) { char *err = NULL; int linenum = 0, totlines, i; sds *lines; // 按换行符分割 lines = sdssplitlen(config,strlen(config),"\n",1,&totlines); for (i = 0; i < totlines; i++) { char *line = lines[i]; linenum++; // 跳过空行和注释 line = strtrim(line); if (line[0] == '#' || line[0] == '\0') continue; // 拆分 key value char *argv[CONFIG_MAX_ARGC]; int argc = 0; // ... 分词逻辑 ... // 开始匹配配置项 if (!strcasecmp(argv[0],"port") && argc == 2) { server.port = atoi(argv[1]); } else if (!strcasecmp(argv[0],"bind") && argc >= 2) { // bind 可以有多个 IP for (j = 0; j < argc-1; j++) { server.bindaddr[server.bindaddr_count++] = zstrdup(argv[j+1]); } } else if (!strcasecmp(argv[0],"maxmemory") && argc == 2) { server.maxmemory = memtoll(argv[1], &err); } // ... 几百个配置项的匹配 ... else { // 配置项不认识 err = "Bad directive or wrong number of arguments"; goto loaderr; } }}
2.1 枚举类型配置
像 maxmemory-policy 这种枚举值,Redis 用一个结构体数组做映射:
typedefstruct configEnum { const char *name; const int val;} configEnum;configEnum maxmemory_policy_enum[] = { {"volatile-lru", MAXMEMORY_VOLATILE_LRU}, {"volatile-lfu", MAXMEMORY_VOLATILE_LFU}, {"volatile-random", MAXMEMORY_VOLATILE_RANDOM}, {"volatile-ttl", MAXMEMORY_VOLATILE_TTL}, {"allkeys-lru", MAXMEMORY_ALLKEYS_LRU}, {"allkeys-lfu", MAXMEMORY_ALLKEYS_LFU}, {"allkeys-random", MAXMEMORY_ALLKEYS_RANDOM}, {"noeviction", MAXMEMORY_NO_EVICTION}, {NULL, 0}};
解析时调用 configEnumGetValue() 遍历数组,找到名字匹配的就返回对应的整数值。
2.2 内存大小配置
maxmemory 1gb 这种带单位的配置,用 memtoll() 函数解析:
long long memtoll(const char *p, int *err) { const char *u; long mul; long long val; // 找到第一个非数字字符 u = p; if (*u == '-') u++; while(*u && isdigit(*u)) u++; // 根据单位计算乘数 if (*u == '\0' || !strcasecmp(u,"b")) { mul = 1; } else if (!strcasecmp(u,"k")) { mul = 1000; } else if (!strcasecmp(u,"kb")) { mul = 1024; } else if (!strcasecmp(u,"m")) { mul = 1000*1000; } else if (!strcasecmp(u,"mb")) { mul = 1024*1024; } else if (!strcasecmp(u,"g")) { mul = 1000L*1000*1000; } else if (!strcasecmp(u,"gb")) { mul = 1024L*1024*1024; } else { if (err) *err = 1; return 0; } ...... // 数字部分转换后乘以单位 val = strtoll(p, NULL, 10); return val * mul;}
这里有个细节:k/m/g 是十进制(1000倍),kb/mb/gb 是二进制(1024倍)。所以 maxmemory 1k 是 1000 字节,maxmemory 1kb 才是 1024 字节。
三、运行时配置修改:CONFIG 命令
Redis 运行中可以用 CONFIG 命令查看和修改配置。
3.1 CONFIG GET
void configCommand(client *c) { // ... } else if (!strcasecmp(c->argv[1]->ptr,"get") && c->argc == 3) { configGetCommand(c); } // ...}
CONFIG GET * 会遍历所有可配置项,返回名字和当前值。实现上就是一堆 addReplyBulkCString() 把配置值写回客户端。
支持模式匹配,比如 CONFIG GET max* 返回所有以 max 开头的配置。
3.2 CONFIG SET
} else if (!strcasecmp(c->argv[1]->ptr,"set") && c->argc == 4) { configSetCommand(c); }
CONFIG SET maxmemory 1073741824 的执行流程:
-
1. 解析参数,拿到配置名和值 -
2. 匹配配置项类型 -
3. 校验值的合法性 -
4. 设置到 server结构体对应字段
源码用宏简化了重复代码,maxmemory 的处理如下:
#define config_set_memory_field(_name,_var) \ } else if (!strcasecmp(c->argv[2]->ptr,_name)) { \ ll = memtoll(o->ptr,&err); \ if (err || ll < 0) goto badfmt; \ _var = ll;// 使用:} config_set_memory_field("maxmemory",server.maxmemory) { if (server.maxmemory) { if (server.maxmemory < zmalloc_used_memory()) { serverLog(LL_WARNING,"WARNING: the new maxmemory value..."); } freeMemoryIfNeeded(); // 设置后立即尝试回收内存 }}
四、配置持久化:CONFIG REWRITE
这个功能在 Redis 2.8.0 引入的。CONFIG SET 改的配置只在内存里,重启就没了。CONFIG REWRITE 可以把当前配置写回配置文件。
4.1 工作原理
int rewriteConfig(char *path) {struct rewriteConfigState *state; // Step 1: 读取旧配置文件 state = rewriteConfigReadOldFile(path); // Step 2: 遍历所有配置项,更新或追加 rewriteConfigYesNoOption(state,"daemonize",server.daemonize,0); rewriteConfigNumericalOption(state,"port",server.port,6379); rewriteConfigBytesOption(state,"maxmemory",server.maxmemory,0); // ... 几百行 ... // Step 3: 生成新文件内容 sds newcontent = rewriteConfigGetContentFromState(state); // Step 4: 覆盖写回 retval = rewriteConfigOverwriteFile(server.configfile,newcontent); return 0;}
关键点:
-
1. 保留注释和格式:读旧文件时记录每一行的类型,写回时尽量保留原有的注释和结构 -
2. 只写非默认值:如果一个配置值等于默认值,且原文件里没有显式写过,就跳过不写 -
3. 原子写入:先写临时文件,再 rename
4.2 覆盖写文件的技巧
int rewriteConfigOverwriteFile(char *configfile, sds content) { int retval = 0; // O_RDWR: 读写模式,O_CREAT: 文件不存在则创建,0644 是权限位 int fd = open(configfile,O_RDWR|O_CREAT,0644); int content_size = sdslen(content), padding = 0;struct stat sb; // 用于获取文件状态信息 sds content_padded; if (fd == -1) return -1; // 打开失败直接返回 if (fstat(fd,&sb) == -1) { // 获取文件状态(大小等) close(fd); return -1; } content_padded = sdsdup(content); // 复制一份,不修改原 content if (content_size < sb.st_size) { // 新内容比旧文件短,需要填充 // 场景:假设旧文件 100 字节,新内容 80 字节 // 如果直接 write 80 字节再 truncate,中间有个时间窗口 // 其他进程读文件会看到 80 字节新内容 + 20 字节旧内容,数据错乱 padding = sb.st_size - content_size; // 需要填充的字节数 content_padded = sdsgrowzero(content_padded,sb.st_size); // 扩展到旧文件大小 content_padded[content_size] = '\n'; // 新内容后先加换行 memset(content_padded+content_size+1,'#',padding-1); // 剩余用 # 填充 // 填充后的内容:新内容 + '\n' + "####..." // 这样写完后,多出来的部分都是注释,解析时会被忽略 } // 单次 write 调用,尽量保证原子性 if (write(fd,content_padded,strlen(content_padded)) == -1) { retval = -1; goto cleanup; } // 如果填充过,现在可以安全地截断到正确长度了 // 此时文件内容已经是完整的,truncate 不会导致数据损坏 if (padding) { if (ftruncate(fd,content_size) == -1) { // 截断失败不是致命错误,文件内容是对的,只是多了一些 # 注释 } }cleanup: sdsfree(content_padded); close(fd); return retval;}
这个填充技巧是为了保证写操作的原子性:如果新内容比旧文件短,直接 truncate 的话,在 write 和 truncate 之间如果有进程读文件会看到不完整内容。用 # 填充后,即使读到多余内容也只是注释,不会解析出错。
五、配置加载时机总结
main() │ ├─ initServerConfig() ← 设置默认值 │ ├─ loadServerConfig() ← 读配置文件 + 命令行参数 │ └─ loadServerConfigFromString() │ ├─ initServer() ← 配置生效(监听端口、设置内存限制等) │ └─ aeMain() ← 进入事件循环
运行时:
CONFIG SET → 修改 server 结构体(内存)CONFIG GET → 读取 server 结构体CONFIG REWRITE → 写回配置文件(磁盘)
六、一个加载的例子
假设 redis.conf 里写了 port 6379,启动时加了 --port 6380:
redis-server redis.conf --port 6380
执行流程:
-
1. initServerConfig()设置server.port = 6379(默认值) -
2. loadServerConfig()读 redis.conf,解析到port 6379,设置server.port = 6379 -
3. 继续解析命令行参数 --port 6380,覆盖为server.port = 6380 -
4. initServer()用server.port创建监听 socket
运行中执行 CONFIG SET port 6381,只是改了内存里的 server.port,不会重新绑定端口(端口只能在启动时设置)。执行 CONFIG REWRITE 后,6381 会写回配置文件,下次启动生效。
夜雨聆风