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

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

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