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