設(shè)置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

Redis 為什么那么快

低并發(fā)編程 2022/10/17 19:39:13 責(zé)編:遠(yuǎn)生

本文來(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之家所有文章均包含本聲明。

相關(guān)文章

關(guān)鍵詞:Redis

軟媒旗下網(wǎng)站: IT之家 最會(huì)買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機(jī)APP應(yīng)用 魔方 最會(huì)買 要知