當咱們對不斷加深對某一項技術的瞭解時,必定會在一個特定的時間對它的實現方式產生興趣。沒錯,這就是我如今的狀態,因此,多年沒有讀/寫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命令執行過程究竟作了哪些事情。