1. Redis 服务器
Redis服务器负责与客户端建立网络连接,处理发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并且通过一系列资源管理措施来维持服务器自身的正常运转。本次主要剖析server.c文件,本文主要介绍Redis服务器的一下几个实现:
- 命令的执行过程
- Redis服务器的周期性任务
- maxmemory的策略
- Redis服务器的main函数
2. 命令的执行过程
Redis一个命令的完整执行过程如下:
- 客户端发送命令请求
- 服务器接收命令请求
- 服务器执行命令请求
- 将回复发送给客户端
关于命令接收与命令回复,在Redis 网络连接库剖析一文已经详细剖析过,本篇主要针对第三步,也就是服务器执行命令的过程进行剖析。
服务器在接收到命令后,会将命令以对象的形式保存在服务器client的参数列表robj **argv中,因此服务器执行命令请求时,服务器已经读入了一套命令参数保存在参数列表中。执行命令的过程对应的函数是processCommand(),源码如下:
我们总结出执行命令的大致过程:
- 查找命令。对应的代码是:c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
- 执行命令前的准备。对应这些判断语句。
- 执行命令。对应代码是:call(c,CMD_CALL_FULL);
我们就大致就这三个过程详细解释。
2.1 查找命令
lookupCommand()函数是对dictFetchValue(server.commands, name);的封装。而这个函数的意思是:从server.commands字典中查找name命令。这个保存命令表的字典,键是命令的名称,值是命令表的地址。因此我们介绍服务器初始化时的一个操作,就是创建一张命令表。命令表代码简化表示如下:
我们只展示了命令表的两条,可以通过COMMAND COUNT命令查看命令的个数。虽然只有两条,但是可以说明问题。
首先命令表是就是一个数组,数组的每个成员都是一个struct redisCommand结构体,对每个数组成员都进行了初始化。我们一次对每个值进行分析:以GET命令为例子。
- char *name:命令的名字。对应 “get”。
- redisCommandProc *proc:命令实现的函数。对应 getCommand。
- int arity:参数个数,-N表示大于等于N。对应2。
- char *sflags:命令的属性,用以下字符作为标识。对应”rF”。
- w:写入命令,会修改数据库。
- r:读取命令,不会修改数据库。
- m:一旦执行会增加内存使用,如果内存短缺则不被允许执行。
- a:管理员命令,例如:SAVE or SHUTDOWN。
- p:发布订阅有关的命令。
- f:强制进行复制的命令,无视服务器的脏键。
- s:不能在脚本中执行的命令。
- R:随机命令。相同的键有相同的参数,在相同的数据库中,可能会有不同的结果。
- S:如果在脚本中调用,那么会对这个命令的输出进行一次排序。
- l:当载入数据库时,允许执行该命令。
- t:从节点服务器持有过期数据时,允许执行的命令。
- M:不能在 MONITOR 下自动传播的命令。
- k:为该命令执行一个隐式的ASKING,所以在集群模式下,如果槽被标记为’importing’,那这个命令会被接收。
*F:快速执行的命令。时间复杂度为O(1) or O(log(N))的命令只要内核调度为Redis分配时间片,那么就不应该在执行时被延迟。
- int flags:sflags的二进制标识形式,可以通过位运算进行组合。对应0。
- redisGetKeysProc *getkeys_proc:从命令中获取键的参数,是一个可选的功能,一般用于三个字段不够执行键的参数的情况。对应NULL。
- int firstkey:第一个参数是 key。对应1。
- int lastkey:最后一个参数是 key。对应1。
- int keystep:从第一个 key 到最后一个 key 的步长。MSET 的步长是 2 因为:key,val,key,val,…。对应1。
- long long microseconds:记录执行命令的耗费总时长。对应0。
- long long calls:记录命令被执行的总次数。对应0。
当从命令表中找到命令后,会将找到的命令的地址,返回给struct redisCommand cmd, lastcmd;这两个指针保存起来。到此查找命令的操作就完成。
2.2 执行命令前的准备
此时,命令已经在命令表中查找到,并且保存在了对应的指针中。但是真正执行前,还进行了许多的情况的判断。我们简单列举几种。
- 首先就是判断命令的参数是否匹配。
- 检查服务器的认证是否通过。
- 集群模式下的判断。
- 服务器最大内存限制是否通过。
- 某些情况下,不接受写命令。
- 发布订阅模式。
- 是否是lua脚本中的命令。
等等……
所以,命令执行的过程还是很复杂的,简单总结一句:命令不易,何况人生。
2.3 执行命令
执行命令调用了call(c,CMD_CALL_FULL)函数,该函数是执行命令的核心。但是不用想,这个函数一定是对回调函数c->cmd->proc(c)的封装,因为proc指向命令的实现函数。我们贴出该函数的代码:
执行命令时,可以指定一个flags。这个flags是用于执行完命令之后的一些后续工作。我们说明这些flags的含义:
执行命令c->cmd->proc(c)就相当于执行了命令实现的函数,然后会在执行完成后,由这些函数产生相应的命令回复,根据回复的大小,会将回复保存在输出缓冲区buf或回复链表repl中。然后服务器会调用writeToClient()函数来将回复写到fd中。详细请看:Redis 网络连接库剖析。
至此,一条命令的执行过程就很清楚明了了。
3. Redis服务器的周期性任务
我们曾经在Redis 事件处理实现一文中说到,Redis的事件分为文件事件(file event)和时间事件(time event)。时间事件虽然是晚于文件事件执行,但是会每隔100ms都会执行一次。话不多说直接上代码:Redis 单机服务器实现源码注释
我们也是大致总结列出部分:
- 主动删除过期的键(也可以在读数据库时被动删除)
- 喂看门狗 watchdog
- 更新一些统计值
- 渐进式rehash
- 触发 BGSAVE / AOF 的重写操作,并处理子进程的中断
- 不同状态的client的超时
- 复制重连
等……
我们重点看两个函数,一个是关于客户端资源管理的clientsCron(),一个是关于数据库资源管理的databasesCron()。
3.1客户端资源管理
服务器要定时检查client是否与服务器有交互,如果超过了设置的限制时间,则要释放client所占用的资源。具体的函数是clientsCronHandleTimeout(),它被clientsCron()函数所调用。
3.2 数据库资源管理
服务器要定时检查数据库的输入缓冲区是否可以resize,以节省内存资源。而resize输入缓冲区的两个条件:
- 输入缓冲区的大小大于32K以及超过缓冲区的峰值的2倍。
- client超过时间大于2秒,且输入缓冲区的大小超过1k
实现的函数是clientsCronResizeQueryBuffer(),被databasesCron()函数所调用。1234567891011121314151617181920212223// resize客户端的输入缓冲区int clientsCronResizeQueryBuffer(client *c) {// 获取输入缓冲区的大小size_t querybuf_size = sdsAllocSize(c->querybuf);// 计算服务器对于client的空转时间,也就是client的超时时间time_t idletime = server.unixtime - c->lastinteraction;// resize输入缓冲区的两个条件:// 1. 输入缓冲区的大小大于32K以及超过缓冲区的峰值的2倍// 2. client超过时间大于2秒,且输入缓冲区的大小超过1kif (((querybuf_size > PROTO_MBULK_BIG_ARG) &&(querybuf_size/(c->querybuf_peak+1)) > 2) ||(querybuf_size > 1024 && idletime > 2)){// 只有输入缓冲区的未使用大小超过1k,则会释放未使用的空间if (sdsavail(c->querybuf) > 1024) {c->querybuf = sdsRemoveFreeSpace(c->querybuf);}}// 清空输入缓冲区的峰值c->querybuf_peak = 0;return 0;}
4. maxmemory的策略
Redis 服务器对内存使用会有一个server.maxmemory的限制,如果超过这个限制,就要通过删除一些键空间来释放一些内存,具体函数对应freeMemoryIfNeeded()。
释放内存时,可以指定不同的策略。策略保存在maxmemory_policy中,他可以指定以下的几个值:
可以看出主要分为三种,
- LRU:优先删除最近最少使用的键。
- TTL:优先删除生存时间最短的键。
- RANDOM:随机删除。
而ALLKEYS和VOLATILE的不同之处就是要确定是从数据库的键值对字典还是过期键字典中删除。
了解了以上这些,我们贴出代码:
5. Redis服务器的main函数
Redis 服务器的main()主要执行了一下操作:
- 初始化服务器状态
- 载入服务器的配置
- 初始化服务器数据结构
- 载入持久化文件还原数据库状态
- 执行事件循环123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186int main(int argc, char **argv) {struct timeval tv;int j;#ifdef INIT_SETPROCTITLE_REPLACEMENTspt_init(argc, argv);#endif// 本函数用来配置地域的信息,设置当前程序使用的本地化信息,LC_COLLATE 配置字符串比较setlocale(LC_COLLATE,"");// 设置线程安全zmalloc_enable_thread_safeness();// 设置内存溢出的处理函数zmalloc_set_oom_handler(redisOutOfMemoryHandler);// 初始化随机数发生器srand(time(NULL)^getpid());// 保存当前信息gettimeofday(&tv,NULL);// 设置哈希函数的种子dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());// 检查开启哨兵模式的两种方式server.sentinel_mode = checkForSentinelMode(argc,argv);// 初始化服务器配置initServerConfig();// 设置可执行文件的绝对路径server.executable = getAbsolutePath(argv[0]);// 分配执行executable文件的参数列表的空间server.exec_argv = zmalloc(sizeof(char*)*(argc+1));server.exec_argv[argc] = NULL;// 保存当前参数for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);// 如果已开启哨兵模式if (server.sentinel_mode) {// 初始化哨兵的配置initSentinelConfig();initSentinel();}// 检查是否执行"redis-check-rdb"检查程序if (strstr(argv[0],"redis-check-rdb") != NULL)redis_check_rdb_main(argc,argv); //该函数不会返回// 解析参数if (argc >= 2) {j = 1; /* First option to parse in argv[] */sds options = sdsempty();char *configfile = NULL;/* Handle special options --help and --version */// 指定了打印版本信息,然后退出if (strcmp(argv[1], "-v") == 0 ||strcmp(argv[1], "--version") == 0) version();// 执行帮助信息,然后退出if (strcmp(argv[1], "--help") == 0 ||strcmp(argv[1], "-h") == 0) usage();// 执行内存测试程序if (strcmp(argv[1], "--test-memory") == 0) {if (argc == 3) {memtest(atoi(argv[2]),50);exit(0);} else {fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");exit(1);}}/* First argument is the config file name? */// 如果第1个参数不是'-',那么是配置文件if (argv[j][0] != '-' || argv[j][1] != '-') {configfile = argv[j];// 设置配置文件的绝对路径server.configfile = getAbsolutePath(configfile);/* Replace the config file in server.exec_argv with* its absoulte path. */zfree(server.exec_argv[j]);// 设置可执行的参数列表server.exec_argv[j] = zstrdup(server.configfile);j++;}// 解析指定的对象while(j != argc) {// 如果是以'-'开头if (argv[j][0] == '-' && argv[j][1] == '-') {/* Option name */// 跳过"--check-rdb"if (!strcmp(argv[j], "--check-rdb")) {/* Argument has no options, need to skip for parsing. */j++;continue;}// 每个选项之间用'\n'隔开if (sdslen(options)) options = sdscat(options,"\n");// 将选项追加在sds中options = sdscat(options,argv[j]+2);// 选项和参数用 " "隔开options = sdscat(options," ");} else {/* Option argument */// 追加选项参数options = sdscatrepr(options,argv[j],strlen(argv[j]));options = sdscat(options," ");}j++;}// 如果开启哨兵模式,哨兵模式配置文件不正确if (server.sentinel_mode && configfile && *configfile == '-') {serverLog(LL_WARNING,"Sentinel config from STDIN not allowed.");serverLog(LL_WARNING,"Sentinel needs config file on disk to save state. Exiting...");exit(1);}// 重置save命令的参数resetServerSaveParams();// 载入配置文件loadServerConfig(configfile,options);sdsfree(options);} else {serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");}// 是否被监视server.supervised = redisIsSupervised(server.supervised_mode);// 是否以守护进程的方式运行int background = server.daemonize && !server.supervised;if (background) daemonize();// 初始化服务器initServer();// 创建保存pid的文件if (background || server.pidfile) createPidFile();// 为服务器进程设置标题redisSetProcTitle(argv[0]);// 打印Redis的logoredisAsciiArt();// 检查backlog队列checkTcpBacklogSettings();// 如果不是哨兵模式if (!server.sentinel_mode) {/* Things not needed when running in Sentinel mode. */serverLog(LL_WARNING,"Server started, Redis version " REDIS_VERSION);#ifdef __linux__// 打印内存警告linuxMemoryWarnings();#endif// 从AOF文件或RDB文件载入数据loadDataFromDisk();// 如果开启了集群模式if (server.cluster_enabled) {// 集群模式下验证载入的数据if (verifyClusterConfigWithData() == C_ERR) {serverLog(LL_WARNING,"You can't have keys in a DB different than DB 0 when in ""Cluster mode. Exiting.");exit(1);}}// 打印端口号if (server.ipfd_count > 0)serverLog(LL_NOTICE,"The server is now ready to accept connections on port %d", server.port);// 打印本地套接字fdif (server.sofd > 0)serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);} else {// 开启哨兵模式,哨兵模式和集群模式只能开启一种sentinelIsRunning();}/* Warning the user about suspicious maxmemory setting. */// 最大内存限制是否配置正确if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);}// 进入事件循环之前执行beforeSleep()函数aeSetBeforeSleepProc(server.el,beforeSleep);// 运行事件循环,一直到服务器关闭aeMain(server.el);// 服务器关闭,删除事件循环aeDeleteEventLoop(server.el);return 0;}