本文來(lái)自微信公眾號(hào):低并發(fā)編程 (ID:dibingfa),原文標(biāo)題:《破玩意 | Redis 為什么那么快》,作者:閃客
我是個(gè) redis 服務(wù),我馬上就要啟動(dòng)了
因?yàn)槲业闹魅苏诳刂婆_(tái)輸入:
/redis-server
宏觀上看下我的流程
突然,主人按下了回車鍵,不得了了。
shell 程序把我的程序加載到了內(nèi)存,開始執(zhí)行我的 main 方法,一切就從這里開始了。
int main(int argc, char **argv) { initServer(); aeCreateFileEvent(fd, acceptHandler, ); aeMain(); }
不要覺得我這里很復(fù)雜,其實(shí)主要就三大步。
第一步,我通過(guò) listenToPort() 方法創(chuàng)建了一個(gè) TCP 連接。
我的這個(gè)方法真是見名知意,而且如果展開看就更會(huì)發(fā)現(xiàn)沒(méi)什么神秘的,就是 socket bind listen 標(biāo)準(zhǔn)三步走,建立了一個(gè) TCP 監(jiān)聽,返回了一個(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í)就是開啟了一個(gè) TCP 監(jiān)聽,然后如果有客戶端進(jìn)來(lái)的話,讓他執(zhí)行 acceptHandler 函數(shù)而已。
之后我就一直死等著客戶端連接了。
void aeMain(aeEventLoop *eventLoop) { eventLoop-stop = 0; while (!eventLoop-stop) aeProcessEvents(eventLoop, AE_ALL_EVENTS); }
展開體驗(yàn)下我的具體工作
此時(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è)專屬的 client 為其服務(wù)。
static redisClient *createClient(int fd) { aeCreateFileEvent(c-fd, readQueryFromClient, ); }
這里又可以看到,所謂的專屬服務(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)聽新客戶端連接的,始終是最上面那個(gè)文件描述符。
好了,服務(wù)端開啟了監(jiān)聽,客戶端也連上了服務(wù)端,此時(shí)我仍然在死等狀態(tài),只不過(guò)等的不只是新客戶端連接到達(dá),還在等待已經(jīng)連接上的客戶端發(fā)來(lái)命令。
請(qǐng)注意,這里的死等,只有一個(gè)線程,循環(huán)調(diào)用 aeProcessEvents 函數(shù),用 select 的方式監(jiān)聽多個(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)聽這些描述符,并做處理。所以即使客戶端并發(fā)地發(fā)送命令,后面仍然是依次取出命令,順序執(zhí)行。
這也就是我們常常說(shuō)的,redis 是單線程的,命令與命令之間是順序執(zhí)行,無(wú)需考慮線程安全的問(wèn)題。
為了方便大家吹牛,我來(lái)拔高一下
大家發(fā)現(xiàn)沒(méi),我的啟動(dòng)過(guò)程,其實(shí)就分成兩個(gè)大的部分。
一個(gè)是監(jiān)聽客戶端的請(qǐng)求,就是用 IO 多路復(fù)用的方式,監(jiān)聽多個(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)聽連接的文件描述符所綁定的函數(shù) acceptHandler。
所謂的命令請(qǐng)求處理器,就是監(jiān)聽客戶端命令(讀事件)的文件描述符綁定的函數(shù) readQueryFromClient。
所謂的命令回復(fù)處理器,就是后面要提到的,監(jiān)聽客戶端響應(yīng)(寫事件)的文件描述符綁定的函數(shù) sendReplyToClient。
這種一個(gè)負(fù)責(zé)響應(yīng) IO 事件,一個(gè)負(fù)責(zé)交給相應(yīng)的事件處理器去處理,就叫做 Reactor 模式。
Redis 正是基于 Reactor 模式開發(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。
這個(gè)函數(shù),最終就會(huì)一步步地將 key 和 value 分別存儲(chǔ)起來(lái)。
處理完命令后,就要發(fā)送響應(yīng)給客戶端了。
static void setCommand(redisClient *c) { addReply(c, nx ? shared.cone : shared.ok); }
這個(gè)響應(yīng),并不是直接同步寫回去,當(dāng)然也不是開啟一個(gè)線程去異步寫回去。
它仍然是調(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ò)程啦,我是不是很可愛。
后記
整篇文章我好像沒(méi)講 Redis 為啥那么快,因?yàn)槲腋杏X這個(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ì)是完全一樣的。
廣告聲明:文內(nèi)含有的對(duì)外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。