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 的执行流程:

  1. 解析参数,拿到配置名和值
  2. 匹配配置项类型
  3. 校验值的合法性
  4. 设置到 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}

关键点:

  1. 保留注释和格式:读旧文件时记录每一行的类型,写回时尽量保留原有的注释和结构
  2. 只写非默认值:如果一个配置值等于默认值,且原文件里没有显式写过,就跳过不写
  3. 原子写入:先写临时文件,再 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

执行流程:

  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 会写回配置文件,下次启动生效。

— END —