引言


上一篇文章中,我们已经搭建好了阅读和调试Redis源码的环境,这篇文章就开始愉快的阅读了。

​ 首先看一下Redis比较简单部分,命令行参数和配置的解析,这是redis-server启动时首先会执行的代码,熟悉了这一部分代码,以后想知道Redis支持哪些配置及其默认值时直接去代码里查找就可以了。

​ 整个redis-server的main函数位于redis.c:3933中,我们从main开始逐步分析代码的流程。

redis.c:3933表示redis.c文件的3933行,撇过一眼Redis源码的都知道Redis所有源代码都是放在一个目录下的,所以这里就直接写文件名了。

Redis中的一些基础设施


​ 对于经常使用Java,Go这些高级语言的人来说,一些本来应该在标准库中的东西,Redis因为C标准库太弱或者是别的什么原因自己重新实现了一遍,如果只是想走马观花地梳理代码大概流程的话,只要知道这些东西大概是什么就可以了,不用细究其原理,其实现其实也和很多高级语言的标准库类似:

/*
 * 保存字符串对象的结构
 * sds: simple dynamic string
 */
struct sdshdr {
    
    // buf 中已占用空间的长度(不包含\0)
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};
/*
 * 字典
 */
typedef struct dict {

    // 类型特定函数   就是一些map扩展点
    dictType *type;

    // 私有数据   传递给类型特定函数的可选参数
    void *privdata;

    // 哈希表  一般情况下只使用ht[0],只有在rehash时使用ht[1]
    dictht ht[2];

    // 记录rehash进度
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

初始化默认配置


​ 在main函数的前几行你就能找到一个initServerConfig函数调用,在这个函数调用中,将初始化redis-server的默认配置,其实就是填充redisServer结构体,从initServerConfig方法中可以读到Redis的各个配置的默认值,其中比较要注意的是命令表的初始化,redis.c:1860-1861

    server.commands = dictCreate(&commandTableDictType,NULL);
    server.orig_commands = dictCreate(&commandTableDictType,NULL);
    populateCommandTable();

​ 上面的dictCreate创建了两个sds->redisCommand的字典 ,可以通过命令的名称直接取到redisCommand结构体redisCommand结构体代表一条Redis指令,结构体中包含该条Redis指令的实现函数,参数个数等等,后面的指令参数校验和执行都会用到这个结构体:

/*
 * Redis 命令
 */
struct redisCommand {

    // 命令名字
    char *name;

    // 实现函数
    redisCommandProc *proc;

    // 参数个数
    int arity;

    // 字符串表示的 FLAG
    char *sflags; /* Flags as string representation, one char per flag. */

    // 实际 FLAG
    int flags;    /* The actual flags, obtained from the 'sflags' field. */

    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    // 从命令中判断命令的键参数。在 Redis 集群转向时使用。
    redisGetKeysProc *getkeys_proc;

    /* What keys should be loaded in background when calling this command? */
    // 指定哪些参数是 key
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */

    // 统计信息
    // microseconds 记录了命令执行耗费的总毫微秒数
    // calls 是命令被执行的总次数
    long long microseconds, calls;
};

dictCreate只是创建了两个空字典,后面的populateCommandTable会给这两个空字典填充一样的内容:

/* Populates the Redis Command Table starting from the hard coded list
 * we have on top of redis.c file. 
 *
 * 根据 redis.c 文件顶部的命令列表,创建命令表
 */
void populateCommandTable(void) {
    int j;

    // 命令的数量
    int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);
    for (j = 0; j < numcommands; j++) {
        
        //...
        
        // 给server.commands和server.orig_commands填充一样的内容
        retval1 = dictAdd(server.commands, sdsnew(c->name), c);
        retval2 = dictAdd(server.orig_commands, sdsnew(c->name), c);
    }
}

​ 从populateCommandTable的代码中可以看出,redisCommandTable这个变量存储了所有Redis支持的指令信息,以后想知道Redis支持哪些指令的话,可以到这个变量里查找:

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
    //...
}

​ 之所以要这里要创建两个一模一样的字典,就是在为后面解析rename-command配置做准备,redis支持在配置文件中通过rename-command命令来重命名甚至彻底取消指令:

# 将CONFIG指令重命名为CC
rename-command CONFIG CC
# 彻底取消CONFIG指令
rename-command CONFIG ""

​ 所以server.orig_commands中保存的就是原始的指令名称与redisCommand的映射关系,而server.commands保存的就是重命名之后的映射关系。

解析用户配置


main执行到redis.c:3965就开始解析用户的配置了,首先是解析命令行参数,如果是-h或者-v,则直接执行并退出:

    if (argc >= 2) {
        int j = 1; /* First option to parse in argv[] */
        sds options = sdsempty();
        char *configfile = NULL; // 配置文件名

        /* Handle special options --help and --version */
        // 处理特殊选项 -h 、-v 和 --test-memory
        if (strcmp(argv[1], "-v") == 0 ||
            strcmp(argv[1], "--version") == 0) version();
        if (strcmp(argv[1], "--help") == 0 ||
            strcmp(argv[1], "-h") == 0) usage();
        //....

​ 之后它会把配置文件(即redis.conf)的路径解析出来赋给configfile变量:

        if (argv[j][0] != '-' || argv[j][1] != '-')
            configfile = argv[j++];

​ 然后把所有的命令行参数都重整成redis.conf里面的配置格式并且追到到options字符串(sds)后面:

        // 对用户给定的其余选项进行分析,并将分析所得的字符串追加稍后载入的配置文件的内容之后
        // 比如 --port 6380 会被分析为 "port 6380\n"
        // 就是在重整成config file格式,然后接到config file的后面
        while(j != argc) {
            if (argv[j][0] == '-' && argv[j][1] == '-') { // --开头
                /* Option name */
                if (sdslen(options)) options = sdscat(options,"\n");
                options = sdscat(options,argv[j]+2);
                options = sdscat(options," ");
            } else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
            }
            j++;
        }

​ 之所以要重整成这样的格式,因为接下来会把options字符串给接到redis.conf的尾部,这也是为什么命令行参数的优先级比redis.conf中配置的高。在redis.c:4017调用loadServerConfig开始解析配置文件:

        loadServerConfig(configfile,options);

loadServerConfig会先将配置文件读出,然后将options接到其尾部,之后一起传递给loadServerConfigFromString

void loadServerConfig(char *filename, char *options) {
    sds config = sdsempty();
    char buf[REDIS_CONFIGLINE_MAX+1];

    /* Load the file content */
    // 载入文件内容
    if (filename) {
        FILE *fp;

        if (filename[0] == '-' && filename[1] == '\0') {
            fp = stdin;
        } else {
            if ((fp = fopen(filename,"r")) == NULL) {
                redisLog(REDIS_WARNING,
                    "Fatal error, can't open config file '%s'", filename);
                exit(1);
            }
        }
        while(fgets(buf,REDIS_CONFIGLINE_MAX+1,fp) != NULL)
            config = sdscat(config,buf);
        if (fp != stdin) fclose(fp);
    }

    /* Append the additional options */
    // 追加 options 字符串到内容的末尾
    if (options) {
        config = sdscat(config,"\n");
        config = sdscat(config,options);
    }

    // 根据字符串内容,设置服务器配置
    loadServerConfigFromString(config);

    sdsfree(config);
}

​ 而loadServerConfigFromString里面有长长的if .. else ...判断,当你想知道某个配置与redisServer结构体中的字段的关系时,查找这里就可以了,config.c:122.

        /* Execute config directives */
        if (!strcasecmp(argv[0],"timeout") && argc == 2) {
            server.maxidletime = atoi(argv[1]);
            if (server.maxidletime < 0) {
                err = "Invalid timeout value"; goto loaderr;
            }
        } else if (!strcasecmp(argv[0],"tcp-keepalive") && argc == 2) {
            server.tcpkeepalive = atoi(argv[1]);
            if (server.tcpkeepalive < 0) {
                err = "Invalid tcp-keepalive value"; goto loaderr;
            }
        } else if (!strcasecmp(argv[0],"port") && argc == 2) {
        //...

End


下一篇文章将讲述网络层的实现,它是Redis单线程高性能的关键。