引言
上一篇文章中,我们已经搭建好了阅读和调试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,和很多高级语言中的buffer类似,能够往里面动态增加内容,如果容量不够会自动扩容一倍
/*
* 保存字符串对象的结构
* sds: simple dynamic string
*/
struct sdshdr {
// buf 中已占用空间的长度(不包含\0)
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
- dict:字典,和高级语言中的hashmap类似,当空间不够时,也会触发
rehash
/*
* 字典
*/
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;
- list:双向链表
初始化默认配置
在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单线程高性能的关键。