戰(zhàn)略上藐視技術(shù),戰(zhàn)術(shù)上重視技術(shù)我是個 redis 服務(wù),我馬上就要啟動了
shell 程序把我的程序加載到了內(nèi)存,開始執(zhí)行我的 main 方法,一切就從這里開始了。int main(int argc, char **argv) {
...
initServer();
...
aeCreateFileEvent(fd, acceptHandler, ...);
...
aeMain();
...
}
第一步,我通過 listenToPort() 方法創(chuàng)建了一個 TCP 連接。
我的這個方法真是見名知意,而且如果展開看就更會發(fā)現(xiàn)沒什么神秘的,就是 socket bind listen 標準三步走,建立了一個 TCP 監(jiān)聽,返回了一個文件描述符 fd。第二步,我通過 aeCreateFileEvent() 方法,將上面那個創(chuàng)建了 TCP 連接返回的文件描述符 fd,加入到一個叫 aeFileEvent的鏈表中。
同時將這個文件描述符綁定一個函數(shù) acceptHandler,這樣當有客戶端連接進來時,便會執(zhí)行這個函數(shù)。
第三步,我通過 aeMain() 方法,將上面的 aeFileEvent 鏈表中的文件描述符,統(tǒng)統(tǒng)作為 select 的入?yún)?,這是 IO 多路復(fù)用模式,如果不太了解的同學(xué)請閱讀,《你管這破玩意叫 IO 多路復(fù)用?》。
好了,其實就是開啟了一個 TCP 監(jiān)聽,然后如果有客戶端進來的話,讓他執(zhí)行 acceptHandler 函數(shù)而已。
void aeMain(aeEventLoop *eventLoop)
{
eventLoop->stop = 0;
while (!eventLoop->stop)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

此時,另外一個人啟動了一個 redis-client,連接到了我。redis-cli -h host -p port
那么我頭上的 fd 就會感知有數(shù)據(jù)讀入,并執(zhí)行 acceptHandler 方法。static void acceptHandler(...) {
...
cfd = anetAccept(...);
...
c = createClient(cfd))
...
}
可以看到,當有新客戶端連接進來時,便會調(diào)用 createClient 創(chuàng)建一個專屬的 client 為其服務(wù)。static redisClient *createClient(int fd) {
...
aeCreateFileEvent(c->fd, readQueryFromClient, ...);
...
}
這里又可以看到,所謂的專屬服務(wù),其實仍然是這個 aeCreateFileEvent 函數(shù)。這個上面說了,這個函數(shù)的功能就是把文件描述符掛在鏈表上,然后分配一個處理函數(shù)。當然,這回的處理函數(shù)不再是處理新客戶端連接的 acceptHandler,而是處理具體客戶端傳來的 redis 命令的函數(shù) readQueryFromClient。
不難想象,如果再來一個客戶端,又來一個客戶端... 那么不斷將新客戶端的文件描述符掛上去即可,而監(jiān)聽新客戶端連接的,始終是最上面那個文件描述符。
好了,服務(wù)端開啟了監(jiān)聽,客戶端也連上了服務(wù)端,此時我仍然在死等狀態(tài),只不過等的不只是新客戶端連接到達,還在等待已經(jīng)連接上的客戶端發(fā)來命令。
請注意,這里的死等,只有一個線程,循環(huán)調(diào)用 aeProcessEvents 函數(shù),用 select 的方式監(jiān)聽多個文件描述符。放上剛剛 main 方法的第三步,幫大家回憶一下。void aeMain(aeEventLoop *eventLoop)
{
eventLoop->stop = 0;
while (!eventLoop->stop)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
當有新客戶端建立連接時,會觸發(fā) acceptHandler 函數(shù)執(zhí)行,多出一個等待數(shù)據(jù)的描述符。
當有客戶端數(shù)據(jù)傳來時,會觸發(fā) readQueryFromClient 函數(shù)執(zhí)行,完成這個命令的操作。
注意,由于只有一個線程在監(jiān)聽這些描述符,并做處理。所以即使客戶端并發(fā)地發(fā)送命令,后面仍然是依次取出命令,順序執(zhí)行。
這也就是我們常常說的,redis 是單線程的,命令與命令之間是順序執(zhí)行,無需考慮線程安全的問題。大家發(fā)現(xiàn)沒,我的啟動過程,其實就分成兩個大的部分。一個是監(jiān)聽客戶端的請求,就是用 IO 多路復(fù)用的方式,監(jiān)聽多個文件描述符,就剛剛那個 aeMain() 方法干的事嘛。一個是執(zhí)行相應(yīng)的函數(shù)去處理這個請求,具體執(zhí)行什么函數(shù)就是出現(xiàn)多次的 aeCreateFileEvent() 方法去綁定的,這個相應(yīng)的函數(shù)說得高大上一點,叫做事件處理器。
這里所謂的連接應(yīng)答處理器,就是剛剛監(jiān)聽連接的文件描述符所綁定的函數(shù) acceptHandler。所謂的命令請求處理器,就是監(jiān)聽客戶端命令(讀事件)的文件描述符綁定的函數(shù) readQueryFromClient。所謂的命令回復(fù)處理器,就是后面要提到的,監(jiān)聽客戶端響應(yīng)(寫事件)的文件描述符綁定的函數(shù) sendReplyToClient。這種一個負責(zé)響應(yīng) IO 事件,一個負責(zé)交給相應(yīng)的事件處理器去處理,就叫做 Reactor 模式。Redis 正是基于 Reactor 模式開發(fā)了自己的文件事件處理器,實現(xiàn)了高性能的網(wǎng)絡(luò)通信模型,并且保持了 Redis 內(nèi)部單線程設(shè)計的簡單性。有點擔(dān)心這句話吹牛的逼格不夠,其實我是參考了《Redis 設(shè)計與實現(xiàn)》,截圖給大家。
現(xiàn)在,我們通過一個已建立好連接的客戶端,發(fā)一個 redis 命令。<client 6379> set dibingfa niubi
此時 readQueryFromClient 函數(shù)將被執(zhí)行。這個函數(shù)會去一張表中尋找命令所對應(yīng)的函數(shù),這部分用的編碼技巧叫命令模式。static struct redisCommand cmdTable[] = {
{"get",getCommand,2,REDIS_CMD_INLINE},
{"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
{"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
{"del",delCommand,-2,REDIS_CMD_INLINE},
{"exists",existsCommand,2,REDIS_CMD_INLINE},
...
}
找到了 set 命令對應(yīng)的函數(shù)就是 setCommand。處理完命令后,就要發(fā)送響應(yīng)給客戶端了。static void setCommand(redisClient *c) {
...
addReply(c, nx ? shared.cone : shared.ok);
}
這個響應(yīng),并不是直接同步寫回去,當然也不是開啟一個線程去異步寫回去。它仍然是調(diào)用那個萬惡的 aeCreateFileEvent 函數(shù),將 sendReplyToClient 函數(shù)掛在需要響應(yīng)的客戶端連接的文件描述符上。static void addReply(redisClient *c, robj *obj) {
...
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c, NULL) == AE_ERR);
}
以上這些個破玩意,就是我的啟動過程啦,我是不是很可愛。
整篇文章我好像沒講 Redis 為啥那么快,因為我感覺這個問題問得不好。
你可以從接收網(wǎng)絡(luò)請求的 IO 多路復(fù)用角度說起,也可以從事件處理器驅(qū)動的 Reactor 模式說起,還可以從具體處理命令時的數(shù)據(jù)結(jié)構(gòu)說起,比如單單是字符串背后的 sds 其實就做了很多的巧妙設(shè)計。
如果我是面試官,我會具體讓面試者聊聊 Redis 的啟動流程,或者 Redis 處理命令的整個流程。
這里面可挖的點挺多的,如果能談笑風(fēng)生,那自然是技術(shù)水平還不錯。
另外,你會發(fā)現(xiàn)本文出現(xiàn)的很多唬人的術(shù)語,比如 Reactor 模式,事件處理器等,看一遍 Redis 源碼后你會發(fā)現(xiàn)真的非常簡單。
毫不客氣地說,一切絲毫不談具體實現(xiàn),和你堆砌一大堆唬人名詞的文章或者人,都是在耍流氓。
本文我參考的是 Redis3.0.0 源碼,但成文時用的講解代碼是 Redis1.0.0,整個網(wǎng)絡(luò)模塊的設(shè)計是完全一樣的。
我整理了一份 Redis 啟動流程的極簡版附注釋代碼的精美 PDF。

大家可以加我好友獲取(低并發(fā)編程 菜單欄),由于我比較懶,直接把它放在朋友圈第一條了,大家直接看我朋友圈獲取即可,哈哈哈。