乐于分享
好东西不私藏

Redis 配置加载机制源码分析

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. 1. 解析参数,拿到配置名和值
  2. 2. 匹配配置项类型
  3. 3. 校验值的合法性
  4. 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. 1. 保留注释和格式:读旧文件时记录每一行的类型,写回时尽量保留原有的注释和结构
  2. 2. 只写非默认值:如果一个配置值等于默认值,且原文件里没有显式写过,就跳过不写
  3. 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. 1. initServerConfig() 设置 server.port = 6379(默认值)
  2. 2. loadServerConfig() 读 redis.conf,解析到 port 6379,设置 server.port = 6379
  3. 3. 继续解析命令行参数 --port 6380,覆盖为 server.port = 6380
  4. 4. initServer() 用 server.port 创建监听 socket

运行中执行 CONFIG SET port 6381,只是改了内存里的 server.port,不会重新绑定端口(端口只能在启动时设置)。执行 CONFIG REWRITE 后,6381 会写回配置文件,下次启动生效。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Redis 配置加载机制源码分析

猜你喜欢

  • 暂无文章