轉載自:http://blog.chinaunix.net/uid-790245-id-3766842.html
在文章的開頭我們把所有服務端文件列出來,並且標示出其作用:
- adlist.c //雙向鏈表
- ae.c //事件驅動
ae_epoll.c //epoll接口, linux用
ae_kqueue.c //kqueue接口, freebsd用
ae_select.c //select接口, windows用 - anet.c //網絡處理
- aof.c //處理AOF文件
- bio.c //后台線程的業務邏輯
- config.c //配置文件解析
- db.c //DB處理
- dict.c //hash表
- intset.c //轉換為數字類型數據
- multi.c //事務,多條命令一起打包處理
- networking.c //讀取、解析和處理客戶端命令
- object.c //各種對像的創建與銷毀,string、list、set、zset、hash
- rdb.c //redis數據文件處理
- redis.c //程序主要文件
- replication.c //數據同步master-slave
- sds.c //字符串處理
- sort.c //用於list、set、zset排序
- t_hash.c //hash類型處理
t_list.c //list類型處理
t_set.c //set類型處理
t_string.c //string類型處理
t_zset.c //zset類型處理 - ziplist.c //節省內存方式的list處理
- zipmap.c //節省內存方式的hash處理
- zmalloc.c //內存管理
上面基本是redis最主要的處理文件,部分沒有列出來,如VM之類的,就不在這里講了。
首先我們來回顧一下redis的一些基本知識:
- redis有N個DB(默認為16個DB),並且每個db有一個hash表負責存放key,同一個DB不能有相同的KEY,但是不同的DB可以相同的KEY;
- 支持的幾種數據類型:string、hash、list、set、zset;
- redis可以使用aof來保存寫操作日志(也可以使用快照方式保存數據文件)
對於數據類型在這里簡單的介紹一下(網上有圖,下面我貼上圖片可能更容易理解)
1、對於一個string對像,直接存儲內容;
2、對於一個hash對像,當成員數量少於512的時候使用zipmap(一種很省內存的方式實現hash table),反之使用hash表(key存儲成員名,value存儲成員數據);
3、對於一個list對像,當成員數量少於512的時候使用ziplist(一種很省內存的方式實現list),反之使用雙向鏈表(list);
4、對於一個set對像,使用hash表(key存儲數據,內容為空)
5、對於一個zset對像,使用跳表(skip list),關於跳表的相關內容可以查看本blog的跳表學習筆記;
下面正式進入源代碼的分析
1、首先是初始化配置,initServerConfig(redis.c:759)
void initServerConfig() { server.port = REDIS_SERVERPORT; server.bindaddr = NULL; server.unixsocket = NULL; server.ipfd = -1; server.sofd = -1;
2.在初始化配置中調用了populateCommandTable(redis.c:925)函數,該函數的目地是將命令集分布到一個hash table中,大家可以看到每一個命令都對應一個處理函數,因為redis支持的命令集還是蠻多,所以如果要靠if分支來做命令處理的話即繁瑣效率還底, 因此放到hash table中,在理想的情況下只需一次就能定位命令的處理函數。
void populateCommandTable(void) { int j; int numcommands = sizeof(readonlyCommandTable)/sizeof(struct redisCommand); for (j = 0; j < numcommands; j++) { struct redisCommand *c = readonlyCommandTable+j; int retval; retval = dictAdd(server.commands, sdsnew(c->name), c); assert(retval == DICT_OK); } }
3、對參數的解析,redis-server有一個參數(可以不需要),這個參數是指定配置文件路徑,然后由函數loadServerConfig(config.c:28)加載所有配置
if (argc == 2) { if (strcmp(argv[1], “-v”) == 0 || strcmp(argv[1], “–version”) == 0)
version(); if (strcmp(argv[1], “–help”) == 0) usage(); resetServerSaveParams(); loadServerConfig(argv[1]);
4、初始化服務器initServer(redis.c:836), 該函數初始化一些服務器信息,包括創建事件處理對像、db、socket、客戶端鏈表、公共字符串等。
void initServer() { int j; signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); setupSignalHandlers();//設置信號處理 if (server.syslog_enabled) { openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,server.syslog_facility); }
5、在上面初始化服務器中有一段代碼是創建事件驅動,aeCreateTimeEvent是創建一個定時器,下面創建的定時器將會每毫秒調用 serverCron函數,而aeCreateFileEvent是創建網絡事件驅動,當server.ipfd和server.sofd有數據可讀的情況將會分別調用函數acceptTcpHandler和acceptUnixHandler。
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL); if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,acceptTcpHandler,NULL) == AE_ERR)
oom(“creating file event”); if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,acceptUnixHandler,NULL) == AE_ERR)
oom(“creating file event”);
6、接下來就是初始化數據,如果開啟了AOF,那么會調用loadAppendOnlyFile(aof.c:216)去加載AOF文件,在AOF 文件中存放了客戶端的命令,函數將數據讀取出來然后依次去調用命令集去處理,當AOF文件很大的時候勢必為影響客戶端的請求,所以每處理1000條命令就會去嘗試接受和處理客戶端的請求,其代碼在aof.c第250行; 但是如果沒有開啟AOF並且有rdb的情況,會調用rdbLoad(redis.c:873)嘗試去加載rdb文件,理所當然的在加載rdb文件的內部也會考慮文件太大而影響客戶端請求,所以跟AOF一樣,每處理1000條也會嘗試去接受和處理客戶端請求。
7、當所有初始化工作做完之后,服務端就開始正式工作了
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
8、大家都知道redis是單線程模式,所有的請求、處理都是在同一個線程里面進行,也就是一個無限循環,在這個無限循環的內部有兩件事要做,第一件就是調用通過aeSetBeforeSleepProc函數設置的回調函數,第二件就是開始接受客戶端的請求和處理,所以我們可以在第7節看到設置了回調函數為beforeSleep,但是這個beforeSleep到底有什么作用呢?我們在第9節再詳細講述。對於aeMain(ae.c:375)就是 整個程序的主要循環。
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
9、在beforeSleep內部一共有三部分,第一部分對vm進行處理(即第一個if塊),這里我們略過;第二部分是釋放客戶端的阻塞操作,在 redis里有兩個命令BLPOP和BRPOP需要使用這些操作(彈出列表頭或者尾,實現方式見t_list.c:862行的 blockingPopGenericCommand函數),當指定的key不存在或者列表為空的情況下,那么客戶端會一直阻塞,直到列表有數據時,服務 端就會去執行lpop或者rpop並返回給客戶端,那么什么時候需要到BLPOP和BRPOP呢?大家平時肯定用redis做過隊列,最常見的處理方式 就是使用llen去判斷隊列有沒有數據,如果有數據就去取N條,然后處理,如果沒有就sleep(3),然后繼續循環,其實這里就可以使用BLPOP或者 BRPOP來輕松實現,而且可以減少請求,具體怎么實現留給大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),這個函 數主要目的是將aofbuf的數據寫到文件,那aofbuf是什么呢?他是AOF的一個緩沖區,所以客戶端的命令都會在處理完后把這些命令追加到這個緩沖 區中,然后待一輪數據處理完之后統一寫到文件(所以aof也是不能100%保證數據不丟失的,因為如果當redis正在處理這些命令的情況下服務就掛掉, 那么這部分的數據是沒有保存到硬盤的),大家都知道寫數據到文件並不是立即寫到硬盤,只是保存到一個文件緩沖區中,什么情況下會把緩沖區的數據轉到硬盤 呢?只要滿足如下三種條件的一種就能將數據真正存到硬盤:
- 手動調用刷新緩沖區;
- 緩沖區已滿;
- 程序正常退出。
因此redis將數據寫到文件緩沖區之后會判斷是否需要刷到硬盤,server.appendfsync有兩種方式,第一種(APPENDFSYNC_ALWAYS):無條件刷新,即每次寫文件都會保存到硬盤,第二種(APPENDFSYNC_EVERYSEC):每隔一秒保存到硬盤。
10、接下來我們開始講解aeProcessEvents(ae.c:275)的處理流程,首先我們來回顧一下第5節設置的定時器和監聽socket事件處理,其中socket事件處理會回調acceptTcpHandler(networking.c:410)和定時器回調函數 serverCron(redis.c:519),在aeProcessEvents的內部有兩部分需要處理,第一部分是調用aeApiPoll判斷 socket是否有數據可讀,整個服務端的socket里面要分監聽socket和客戶端socket,當有客戶端鏈接服務器時,會觸發監聽socket 的事件處理函數,也就是acceptTcpHandler,而acceptTcpHandler會去調用 createClient(networking.c:13)創建客戶端對像,然后為這個客戶端設置事件處理函數 readQueryFromClient(networking.c:827),所以當客戶端有消息時就會觸發客戶端socket 事件處理函數,處理數據部分講在后面詳細講解,接下來的第二部分就是定時器,每次在socket部分處理完后就用調用processTimeEvents(ae.c:212)來處理定時器,那么內部實現也很簡單,當設置定時器的時候就會計算好應該觸發的時間,所以這里就 只需要判斷當前時間是否大於或者等於應該觸發的時間即可。那么這個定時器到底做了什么呢?請繼續第11節。
11、我們繼續跟蹤源代碼serverCron(redis.c:519),整個函數分為七個部分,
- 第一部分:在服務端打印一些關於DB的信息(包括key數量,內存使用量等);
- 第二部分:判斷DB的hash table是否需要擴展大小tryResizeHashTables(redis.c:432);
- 第三部分:關閉太長時間沒有通信的鏈接 closeTimedoutClients(networking.c:629);
- 第四部分:保存rdb文件 rdbSaveBackground(rdb.c:507),當然也是在需要保存的情況才會保存,即設置save參數;
- 第五部分:清除過期的key,當然 這里不是清除全部,他只是隨機取出一些activeExpireCycle(redic.c:477);
- 第六部分:虛擬內存交換部分,將一部分key轉到 虛擬內存中,這里的key也是隨機抽取的, vmSwapOneObjectBlocking(vm.c:521);
- 第七部分:主從同 步,replicationCron(replication.c:500)。
12、在第10節中我們講到客戶端socket處理函數readQueryFromClient,這里我們一層層分析,首先是從客戶端讀取數據,然后調用processInputBuffer,在內部先是判斷類型,然后調用processInlineBuffer或者 processMultibulkBuffer解析參數,解析后的參數由argv存儲參數,其類型是一個指向指針的指針,其中argv[0]是命令名稱, 后面就是命令參數,argc存儲參數數量;然后調用processCommand(redis.c:979)處理命令,在內部調用 lookupCommand(redis.c:940)獲取命令對應的函數,然后調用freeMemoryIfNeeded(redis.c:1385) 判斷是否需要釋放一些內存,接下來就是調用call(redis.c:954)去執行命令,執行命令后會調用 feedAppendOnlyFile(aof.c:137)把命令行保存到aofbuf中,然后判斷是否需要同步數據到slave,如果需要則調用 replicationFeedSlaves(replication.c:10),接下來就是判斷是否需要將數據發送到監控端,如果需要則調用 replicationFeedMonitors(replication.c:82),到這里整個服務流程就結束了。至於每條命令如何執行,大家可以去 查看以t_開頭的幾個文件。下面是一張整個服務的流程圖。