Redis源碼簡要分析

在文章的開頭我們把所有服務端文件列出來,並且標示出其作用:
adlist.c //
雙向鏈表
ae.c //
事件驅動
ae_epoll.c //epoll
接口, linux
ae_kqueue.c //kqueue
接口, freebsd
ae_select.c //select
接口, windows
anet.c //
網絡處理
aof.c //
處理AOF文件
config.c //
配置文件解析
db.c //DB
處理
dict.c //hash

intset.c //
轉換爲數字類型數據
multi.c //
事務,多條命令一起打包處理
networking.c //
讀取、解析和處理客戶端命令
object.c //
各種對像的創建與銷燬,stringlistsetzsethash
rdb.c //redis
數據文件處理
redis.c //
程序主要文件
replication.c //
數據同步master-slave
sds.c //
字符串處理
sort.c //
用於listsetzset排序
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的一些基本知識:
1
redisNDB(默認爲16DB),並且每個db有一個hash表負責存放key,同一個DB不能有相同的KEY,但是不同的DB可以相同的KEY;
2
、支持的幾種數據類型:stringhashlistsetzset;
3
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), 該函數初始化一些服務器信息,包括創建事件處理對像、dbsocket、客戶端鏈表、公共字符串等。
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.ipfdserver.sofd有數據可讀的情 況將會分別調用函數acceptTcpHandleracceptUnixHandler
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.c250但是如果沒有開啓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裏有兩個命令BLPOPBRPOP需要使用這些操作(彈出列表頭或者尾,實現方式見t_list.c:862行的 blockingPopGenericCommand函數),當指定的key不存在或者列表爲空的情況下,那麼客戶端會一直阻塞,直到列表有數據時,服務 端就會去執行lpop或者rpop並返回給客戶端,那麼什麼時候需要用到BLPOPBRPOP呢?大家平時肯定用redis做過隊列,最常見的處理方式 就是使用llen去判斷隊列有沒有數據,如果有數據就去取N條,然後處理,如果沒有就sleep(3),然後繼續循環,其實這裏就可以使用BLPOP或者 BRPOP來輕鬆實現,而且可以減少請求,具體怎麼實現留給大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),這個函 數主要目的是將aofbuf的數據寫到文件,那aofbuf是什麼呢?他是AOF的一個緩衝區,所以客戶端的命令都會在處理完後把這些命令追加到這個緩衝 區中,然後待一輪數據處理完之後統一寫到文件(所以aof也是不能100%保證數據不丟失的,因爲如果當redis正在處理這些命令的情況下服務就掛掉, 那麼這部分的數據是沒有保存到硬盤的),大家都知道寫數據到文件並不是立即寫到硬盤,只是保存到一個文件緩衝區中,什麼情況下會把緩衝區的數據轉到硬盤 呢?只要滿足如下三種條件的一種就能將數據真正存到硬盤:1、手動調用刷新緩衝區;2、緩衝區已滿;3、程序正常退出。因此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數量,內存使用量等);第二部分:判斷DBhash 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_開頭的幾個文件。下面是一張整個服務的流程圖。

注:redis源代碼爲2.2.8,希望大家看文章的時候配合源代碼一起看,更容易理解


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章