当咱们对不断加深对某一项技术的了解时,必定会在一个特定的时间对它的实现方式产生兴趣。没错,这就是我如今的状态,因此,多年没有读/写C语言的我,决定要啃一下Redis的源码。html
Redis大致上能够分为两部分:服务器和客户端(读者吐槽:你这分的也太大致了吧)。在使用时,咱们先启动服务器,而后再启动客户端。由客户端向服务器发送命令,服务器处理后将结果返回给客户端。咱们从“头”开始,一块儿来了解一下Redis服务器在启动的时候都作了哪些事情。git
对于C语言来讲,main函数是一个程序的的入口,Redis也不例外。Redis的main函数写在server.c文件中。因为redis启动过程至关复杂,须要判断许多条件,例如是否在集群中,或者是不是哨兵模式等等,所以咱们只介绍单机redis启动过程当中一些比较重要的步骤。github
若是redis-server命令启动时使用了test参数,那么就会先进行指定的测试。接下来调用了initServerConfig()函数,这个函数初始化了一个类型为redisServer的全局变量server。redisServer这个结构包含了很是多的字段,因为篇幅限制,咱们不在这里列出,若是按类别划分的话,能够分为如下类别:redis
若是用一句话来归纳initServerConfig()函数做用,它就是用来给能够在配置文件(一般命名为redis.conf)中配置的变量初始化一个默认值。比较经常使用的变量有服务器端口号、日志等级等等。算法
在initServerConfig()函数中,会调用populateCommandTable()函数来设置服务器的命令表,命令表的结构以下。数据库
struct redisCommand redisCommandTable[] = {
{"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
...
}
复制代码
每一项表明的含义是:服务器
设置好命令表后,redis-server还会对一些经常使用的命令设置快速查找方式,直接赋予server的成员指针。app
server.delCommand = lookupCommandByCString("del");
server.multiCommand = lookupCommandByCString("multi");
server.lpushCommand = lookupCommandByCString("lpush");
server.lpopCommand = lookupCommandByCString("lpop");
server.rpopCommand = lookupCommandByCString("rpop");
server.zpopminCommand = lookupCommandByCString("zpopmin");
server.zpopmaxCommand = lookupCommandByCString("zpopmax");
server.sremCommand = lookupCommandByCString("srem");
server.execCommand = lookupCommandByCString("exec");
server.expireCommand = lookupCommandByCString("expire");
server.pexpireCommand = lookupCommandByCString("pexpire");
server.xclaimCommand = lookupCommandByCString("xclaim");
server.xgroupCommand = lookupCommandByCString("xgroup");
复制代码
变量初始化之后,就会将启动命令的路径和参数保存起来,以备下次重启的时候使用。若是启动的服务是哨兵模式,那么就会调用initSentinelConfig()和initSentinel()这两个方法来初始化哨兵模式。对sentinel不了解的同窗能够看这里。initSentinelConfig()和initSentinel()都在sentinel.c文件中。initSentinelConfig函数负责初始化sentinel的端口号,以及解除服务器的保护模式。initSentinel函数负责将command table设置为只支持sentinel命令,以及初始化sentinelState数据格式。less
启动模式若是是redis-check-rdb/aof,那么就会执行redis_check_rdb_main()或redis_check_aof_main()这两个函数来修复持久化文件,不过redis_check_rdb_main函数所作的事情在Redis启动过程当中已经作了,因此这里不须要作,直接使这个函数加载错误就能够了。ide
若是是简单的参数例如-v或--version、-h或--help,就会直接调用相应的方法,打印信息。若是是使用其余配置文件,则修改server.exec_argv。对于其余信息,会将他们转换成字符串,而后添加进配置文件,例如“--port 6380”就会被转换成“port 6380\n”加进配置文件。这时,redis就会调用loadServerConfig()函数来加载配置文件,这个过程会覆盖掉前面初始化默认配置文件的变量的值。
initServer()函数负责结束server变量初始化工做。首先设置处理信号(SIGHUP和SIGPIPE除外),接着会建立一些双向列表用来跟踪客户端、从节点等。
server.current_client = NULL;
server.clients = listCreate();
server.clients_index = raxNew();
server.clients_to_close = listCreate();
server.slaves = listCreate();
server.monitors = listCreate();
server.clients_pending_write = listCreate();
server.slaveseldb = -1; /* Force to emit the first SELECT command. */
server.unblocked_clients = listCreate();
server.ready_keys = listCreate();
server.clients_waiting_acks = listCreate();
复制代码
createSharedObjects()函数会建立一些shared对象保存在全局的shared变量中,对于不一样的命令,可能会有相同的返回值(好比报错)。这样在返回时就没必要每次都去新增对象了,保存到内存中了。这个设计就是以Redis启动时多消耗一些时间为代价,换取运行的更小的延迟。
shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
shared.err = createObject(OBJ_STRING,sdsnew("-ERR\r\n"));
shared.emptybulk = createObject(OBJ_STRING,sdsnew("$0\r\n\r\n"));
shared.czero = createObject(OBJ_STRING,sdsnew(":0\r\n"));
shared.cone = createObject(OBJ_STRING,sdsnew(":1\r\n"));
shared.cnegone = createObject(OBJ_STRING,sdsnew(":-1\r\n"));
shared.nullbulk = createObject(OBJ_STRING,sdsnew("$-1\r\n"));
shared.nullmultibulk = createObject(OBJ_STRING,sdsnew("*-1\r\n"));
shared.emptymultibulk = createObject(OBJ_STRING,sdsnew("*0\r\n"));
shared.pong = createObject(OBJ_STRING,sdsnew("+PONG\r\n"));
shared.queued = createObject(OBJ_STRING,sdsnew("+QUEUED\r\n"));
shared.emptyscan = createObject(OBJ_STRING,sdsnew("*2\r\n$1\r\n0\r\n*0\r\n"));
shared.wrongtypeerr = createObject(OBJ_STRING,sdsnew(
"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"));
shared.nokeyerr = createObject(OBJ_STRING,sdsnew(
"-ERR no such key\r\n"));
复制代码
除了上述的一些返回值之外,createSharedObjects()函数还会建立一些共享的整数对象。对Redis来讲,有许多类型(好比lists或者sets)都须要一些整数(好比数量),这时就能够复用这些已经建立好的整数对象,而不须要从新分配内存并建立。这一样是牺牲了启动时间来换取运行时间。
initServer()函数调用aeCreateEventLoop()函数(ae.c文件)来增长循环事件,并将结果返回给server的el成员。Redis使用不一样的函数来兼容各个平台,在Linux平台使用epoll,在BSD使用kqueue,都不是的话,最终会使用select。Redis轮询新的链接以及I/O事件,有新的事件到来时就会及时做出响应。
Redis初始化须要的数据库,并将结果赋给server的db成员。
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
复制代码
listenToPort()用来初始化一些文件描述符,从而监听server配置的地址和端口。listenToPort函数会根据参数中的地址判断要监听的是IPv4仍是IPv6,对应的调用anetTcpServer()或anetTcp6Server()函数,若是参数中未指明地址,则会强行绑定0.0.0.0
evictionPoolAlloc()(evict.c文件中)用于初始化LRU的键池,Redis的key过时策略是近似LRU算法。
void evictionPoolAlloc(void) {
struct evictionPoolEntry *ep;
int j;
ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
for (j = 0; j < EVPOOL_SIZE; j++) {
ep[j].idle = 0;
ep[j].key = NULL;
ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
ep[j].dbid = 0;
}
EvictionPoolLRU = ep;
}
复制代码
initServer()函数接下来会为数据库和pub/sub再生成一些列表和字典,重置一些状态,标记系统启动时间。在这以后,Redis会执行aeCreateTimeEvent()(在ae.c文件中)函数,用来新建一个循环执行serverCron()函数的事件。serverCron()默认每100毫秒执行一次。
/* Create the timer callback, this is our way to process many background * operations incrementally, like clients timeout, eviction of unaccessed * expired keys and so forth. */
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
复制代码
能够看到,代码中建立循环事件时指定每毫秒执行一次serverCron()函数,这是为了使循环立刻启动,可是serverCron()函数的返回值又会被做为下次执行的时间间隔。默认为1000/server.hz。server.hz随着客户端数量的增长而增长。
serverCron()函数作了许多定时执行的任务,包括rehash、后台持久化,AOF从新与清理、清理过时key,交换虚拟内存、同步主从节点等等。总之能想到的Redis的定时任务几乎都在serverCron()函数中处理。
/* Open the AOF file if needed. */
if (server.aof_state == AOF_ON) {
server.aof_fd = open(server.aof_filename,
O_WRONLY|O_APPEND|O_CREAT,0644);
if (server.aof_fd == -1) {
serverLog(LL_WARNING, "Can't open the append-only file: %s",
strerror(errno));
exit(1);
}
}
复制代码
对于32位系统,最大内存是4GB,若是用户没有明确指出Redis可以使用的最大内存,那么这里默认限制为3GB。
/* 32 bit instances are limited to 4GB of address space, so if there is * no explicit limit in the user provided configuration we set a limit * at 3 GB using maxmemory with 'noeviction' policy'. This avoids * useless crashes of the Redis instance for out of memory. */
if (server.arch_bits == 32 && server.maxmemory == 0) {
serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
复制代码
若是Redis被设置为后台运行,此时Redis会尝试写pid文件,默认路径是/var/run/redis.pid。这时,Redis服务器已经启动,不过还有一些事情要作。
若是存在AOF文件或者dump文件(都有的话AOF文件的优先级高),loadDataFromDisk()函数负责将数据从磁盘加载到内存。
每次进入循环事件时,要调用beforeSleep()函数,它作了如下这些事情:
程序调用aeMain()函数,进入主循环,这时其余的一些循环事件也会分别被调用
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
复制代码
到此,Redis服务器已经彻底准备好处理各类事件了。后面咱们会继续了解Redis命令执行过程究竟作了哪些事情。