Redis憑什么用單線程“干翻”了全世界?
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
Redis (Remote Dictionary Server),即遠(yuǎn)程字典服務(wù),是一個(gè)開(kāi)源的、使用 C 語(yǔ)言編寫(xiě)的、高性能的內(nèi)存鍵值 (Key-Value) 數(shù)據(jù)庫(kù)。 在工程實(shí)踐中,想必你也或多或少都接觸過(guò)它,但是,Redis作為“單線程”應(yīng)用,為什么它會(huì)這么快?你可能會(huì)說(shuō)因?yàn)橛昧藘?nèi)存,其實(shí)這只是其中一點(diǎn),下邊讓我們一起揭曉答案吧。 這里要特別指出的是,Redis的“單線程”,主要是指網(wǎng)絡(luò)IO和實(shí)際的key-value讀寫(xiě)是由一個(gè)線程完成的,但是,持久化、數(shù)據(jù)刪除、AOF重寫(xiě)等,其實(shí)還是由額外的線程執(zhí)行的。 并且,從Redis 6.0開(kāi)始,網(wǎng)絡(luò)IO也采用多線程,只有讀寫(xiě)命令是用單線程處理。 01 “3個(gè)因素”造就了Redis的高性能 Redis以其出色的性能和靈活性,常被稱為數(shù)據(jù)處理領(lǐng)域的“瑞士軍刀”,常用于緩存、會(huì)話存儲(chǔ)、消息隊(duì)列、分布式鎖等場(chǎng)景。 Redis高性能的根因可以歸納為以下3點(diǎn): 內(nèi)存為王-物理定律上的優(yōu)勢(shì) 這是 Redis 高性能的基石,Redis數(shù)據(jù)存放在內(nèi)存中,是它與其他基于磁盤(pán)的數(shù)據(jù)庫(kù)(如 MySQL, PostgreSQL, MongoDB)的重要區(qū)別之一。 極致的數(shù)據(jù)結(jié)構(gòu) Redis內(nèi)部對(duì)哈希表、跳表等數(shù)據(jù)結(jié)構(gòu)做了特別優(yōu)化,會(huì)根據(jù)數(shù)據(jù)的大小和類型,動(dòng)態(tài)選擇最優(yōu)的內(nèi)部編碼,實(shí)現(xiàn)“空間換時(shí)間”或“時(shí)間換空間”,極大地減小了網(wǎng)絡(luò)開(kāi)銷和客戶端的壓力。 高效的IO模型 采用了IO多路復(fù)用機(jī)制,使其在網(wǎng)絡(luò) IO 操作中能并發(fā)處理大量的客戶端請(qǐng)求,實(shí)現(xiàn)高吞吐率。 前兩點(diǎn)的物理內(nèi)存的優(yōu)勢(shì)和優(yōu)化的數(shù)據(jù)結(jié)構(gòu),比較好理解,今天我們重點(diǎn)說(shuō)的是Redis的IO多路復(fù)用模型。 02 為什么要用IO多路復(fù)用? 理解IO模型前,我們來(lái)看一下,IO多路復(fù)用到底能解決什么問(wèn)題? 一句話概括: IO多路復(fù)用(I/O Multiplexing) 是一種機(jī)制,它允許一個(gè)單獨(dú)的進(jìn)程(或線程)監(jiān)視多個(gè)文件描述符(File Descriptor, FD),一旦其中任何一個(gè)FD準(zhǔn)備好進(jìn)行IO操作(比如可讀、可寫(xiě)或出現(xiàn)異常),該機(jī)制就會(huì)通知相應(yīng)的進(jìn)程。 為了更好地理解,我們用一個(gè)經(jīng)典的場(chǎng)景舉例: 一個(gè)網(wǎng)絡(luò)服務(wù)器需要同時(shí)處理成百上千個(gè)客戶端連接,會(huì)面臨的3種選擇: 1、阻塞IO + 每次連接一個(gè)新的進(jìn)程/線程模型 工作方式: 每當(dāng)一個(gè)新客戶端連接進(jìn)來(lái),服務(wù)器就創(chuàng)建一個(gè)新的進(jìn)程或線程專門(mén)為它服務(wù)。這個(gè)線程在等待客戶端數(shù)據(jù)時(shí),會(huì)調(diào)用read()或recv(),然后阻塞(block),直到數(shù)據(jù)到達(dá)。 缺點(diǎn): 資源消耗巨大: 每個(gè)進(jìn)程/線程都需要消耗內(nèi)存(如??臻g)和CPU資源。成千上萬(wàn)個(gè)連接就意味著成千上萬(wàn)個(gè)線程,系統(tǒng)根本無(wú)法承受。 上下文切換開(kāi)銷大: CPU需要在這些大量的線程之間頻繁切換,這本身就是一筆巨大的開(kāi)銷,導(dǎo)致實(shí)際用于處理業(yè)務(wù)邏輯的時(shí)間大大減少。 2、非阻塞IO + 忙輪詢(Busy-Polling) 工作方式: 將每個(gè)連接的IO操作設(shè)置為非阻塞模式。然后用一個(gè)循環(huán),不斷地去輪詢(“詢問(wèn)”)每一個(gè)連接:“你有數(shù)據(jù)要讀嗎?” “你可以寫(xiě)數(shù)據(jù)了嗎?” 缺點(diǎn): CPU空轉(zhuǎn): 即使大部分連接都沒(méi)有事件發(fā)生,循環(huán)也會(huì)一直運(yùn)行,不停地做無(wú)用功,導(dǎo)致CPU使用率100%,造成巨大的浪費(fèi)。 3、IO多路復(fù)用 工作方式: 應(yīng)用進(jìn)程將一批需要監(jiān)視的文件描述符(代表所有客戶端連接)“注冊(cè)”給內(nèi)核; 應(yīng)用進(jìn)程調(diào)用一個(gè)阻塞函數(shù)(如select(), poll(), epoll_wait()),然后自己就去“睡覺(jué)”了,不占用CPU; 內(nèi)核開(kāi)始作為“代理”或“管家”,在底層持續(xù)監(jiān)視這些FD。當(dāng)任何一個(gè)或多個(gè)FD準(zhǔn)備就緒(例如,某個(gè)客戶端發(fā)來(lái)了數(shù)據(jù)),內(nèi)核就會(huì)喚醒正在“睡覺(jué)”的應(yīng)用進(jìn)程; 應(yīng)用進(jìn)程被喚醒后,返回的結(jié)果會(huì)明確告訴它哪些FD已經(jīng)準(zhǔn)備好了; 應(yīng)用進(jìn)程只需要處理那些真正準(zhǔn)備就緒的連接,進(jìn)行讀寫(xiě)操作。 優(yōu)勢(shì): IO多路復(fù)用就是為了解決以上兩個(gè)模型的痛點(diǎn)而生的。 它引入了一個(gè)“代理”或“協(xié)調(diào)者”(即內(nèi)核中的select, poll, epoll等機(jī)制)。 這樣,一個(gè)線程就能高效地管理海量的連接,既避免了多線程的資源和切換開(kāi)銷,也避免了忙輪詢的CPU浪費(fèi)。 03 Linux 3種IO多路復(fù)用實(shí)現(xiàn)方式 Linux系統(tǒng)為我們提供了3種主要的IO多復(fù)用API,即經(jīng)常能看到的select、poll、epoll。 1、select select 是最早的、最經(jīng)典的多路復(fù)用實(shí)現(xiàn),遵循POSIX標(biāo)準(zhǔn),因此可移植性最好。 工作原理: 創(chuàng)建一個(gè)fd_set(一個(gè)位圖結(jié)構(gòu)),把要監(jiān)視的FD對(duì)應(yīng)位置為1; 調(diào)用select( )函數(shù),將這個(gè)fd_set從用戶空間拷貝到內(nèi)核空間; 內(nèi)核遍歷所有被監(jiān)視的FD,檢查它們的狀態(tài); select( )返回后,內(nèi)核會(huì)將準(zhǔn)備就緒的FD對(duì)應(yīng)位置為1,未就緒的置為0,再將修改后的fd_set拷貝回用戶空間; 用戶進(jìn)程需要再次遍歷整個(gè)fd_set,找出哪些FD是準(zhǔn)備就緒的。 缺點(diǎn): 文件描述符數(shù)量限制: fd_set的大小是固定的(通常是1024),限制了能監(jiān)視的FD數(shù)量。 重復(fù)拷貝開(kāi)銷: 每次調(diào)用select都需要在用戶空間和內(nèi)核空間之間來(lái)回拷貝fd_set,連接數(shù)越多,開(kāi)銷越大。 線性掃描開(kāi)銷: 內(nèi)核和用戶進(jìn)程都需要遍歷整個(gè)FD集合來(lái)查找就緒的FD,效率是O(n),其中n是監(jiān)視的FD總數(shù)。即使只有一個(gè)FD就緒,也得全部掃一遍。 2、poll poll 是對(duì)select的改進(jìn),解決了FD數(shù)量限制的問(wèn)題。 工作原理: 它使用一個(gè)pollfd結(jié)構(gòu)體數(shù)組來(lái)代替fd_set。這個(gè)數(shù)組沒(méi)有固定大小限制,可以動(dòng)態(tài)分配。 工作流程與select類似,仍然需要將整個(gè)pollfd數(shù)組從用戶空間拷貝到內(nèi)核空間,并且返回后需要遍歷數(shù)組來(lái)查找就緒的FD。 優(yōu)點(diǎn): 沒(méi)有FD數(shù)量限制,只受限于系統(tǒng)資源。 缺點(diǎn): 仍然存在重復(fù)拷貝和線性掃描的開(kāi)銷,性能問(wèn)題在連接數(shù)巨大時(shí)依然存在。 3、epoll(Event Poll) epoll 是Linux下對(duì)select和poll的重大改進(jìn),是目前公認(rèn)的在Linux上實(shí)現(xiàn)高性能網(wǎng)絡(luò)服務(wù)器的首選。Nginx、Redis等都使用了它。 工作原理: epoll 的設(shè)計(jì)思想完全不同,它引入了三個(gè)核心函數(shù): epoll_create( ) 在內(nèi)核中創(chuàng)建一個(gè)epoll實(shí)例(可以想象成一個(gè)事件中心),返回一個(gè)代表該實(shí)例的FD。這個(gè)實(shí)例內(nèi)部包含一個(gè)紅黑樹(shù)(用于快速查找FD)和一個(gè)就緒鏈表(用于存放已就緒的FD)。 epoll_ctl( ) 用于向epoll實(shí)例中添加、修改或刪除要監(jiān)視的FD。當(dāng)注冊(cè)一個(gè)FD時(shí),內(nèi)核會(huì)將這個(gè)FD和一個(gè)回調(diào)函數(shù)關(guān)聯(lián)起來(lái)。當(dāng)該FD就緒時(shí),內(nèi)核會(huì)自動(dòng)調(diào)用這個(gè)回調(diào)函數(shù),將其放入就緒鏈表中。 epoll_wait( ) 阻塞等待,直到epoll實(shí)例的就緒鏈表非空。一旦有就緒的FD,epoll_wait就會(huì)被喚醒,并直接返回就緒FD的列表。 優(yōu)點(diǎn): 沒(méi)有FD數(shù)量限制。 避免重復(fù)拷貝: epoll_ctl將FD注冊(cè)到內(nèi)核后,就不需要每次調(diào)用epoll_wait時(shí)都重復(fù)拷貝FD列表了。它基于事件驅(qū)動(dòng),由內(nèi)核來(lái)記錄和跟蹤FD狀態(tài)。 效率極高: epoll_wait返回時(shí),直接返回的就是就緒的FD列表,用戶進(jìn)程無(wú)需再遍歷整個(gè)集合。其時(shí)間復(fù)雜度是O(1),與監(jiān)視的FD總數(shù)無(wú)關(guān),只與活躍的FD數(shù)量有關(guān)。 04 Redis的IO模型 1、Redis網(wǎng)絡(luò)IO處理中的核心步驟 以一個(gè)GET請(qǐng)求為例,Redis 網(wǎng)絡(luò)IO處理大概有如下過(guò)程: 網(wǎng)絡(luò)IO處理流程
2、非阻塞模式設(shè)置 上述函數(shù)中, bind() 綁定地址和端口,是本地操作,幾乎瞬間完成。 listen() 將套接字轉(zhuǎn)換為監(jiān)聽(tīng)狀態(tài),也是一個(gè)本地內(nèi)核操作,立即返回。 parse()和get() 都偏向于應(yīng)用層邏輯。 但在默認(rèn)情況下,accept()、recv()、send()是阻塞模式的,也就是說(shuō),當(dāng) Redis 監(jiān)聽(tīng)到一個(gè)客戶端有連接請(qǐng)求,但一直未能成功建立起連接時(shí),會(huì)阻塞在 accept() 函數(shù)這里,導(dǎo)致其他客戶端無(wú)法和 Redis 建立連接。類似的,當(dāng) Redis 通過(guò) recv()/send() 從一個(gè)客戶端讀取或返回?cái)?shù)據(jù)時(shí),如果數(shù)據(jù)一直沒(méi)有到達(dá)或返回,Redis 也會(huì)一直阻塞在 recv()/send()。 不過(guò),幸運(yùn)的是,socket 網(wǎng)絡(luò)模型本身支持非阻塞模式。 在網(wǎng)絡(luò) I/O 處理中,將套接字(Socket)設(shè)置為非阻塞模式,主要影響的是那些需要“等待”網(wǎng)絡(luò)事件的函數(shù)。 一個(gè)套接字一旦通過(guò) fcntl() 或 ioctl() 設(shè)置為非阻塞模式,所有作用于該套接字上的 I/O 函數(shù)都會(huì)變成非阻塞的。 (1)accept() - 接受連接
(2)recv() - 接收數(shù)據(jù)
(3)send() - 發(fā)送數(shù)據(jù)
3、Redis實(shí)現(xiàn) Redis的epoll機(jī)制 毫無(wú)疑問(wèn),Redis采用select/epoll模式來(lái)提升進(jìn)行IO操作。 上圖中的多個(gè) FD 就是多個(gè)套接字。Redis 網(wǎng)絡(luò)框架調(diào)用 epoll 機(jī)制,讓內(nèi)核監(jiān)聽(tīng)這些套接字。此時(shí),Redis 線程不會(huì)阻塞在某一個(gè)特定的監(jiān)聽(tīng)或已連接套接字上,也就是說(shuō),不會(huì)阻塞在某一個(gè)特定的客戶端請(qǐng)求處理上。正因?yàn)榇?,Redis 可以同時(shí)和多個(gè)客戶端連接并處理請(qǐng)求,從而提升并發(fā)性。 為了在請(qǐng)求到達(dá)時(shí)能通知到 Redis 線程,select/epoll 提供了基于事件的回調(diào)機(jī)制,即針對(duì)不同事件的發(fā)生,調(diào)用相應(yīng)的處理函數(shù)。 那么,回調(diào)機(jī)制是怎么工作的呢? 其實(shí),select/epoll 一旦監(jiān)測(cè)到 FD 上有請(qǐng)求到達(dá)時(shí),就會(huì)觸發(fā)相應(yīng)的事件。 這些事件會(huì)被放進(jìn)一個(gè)事件隊(duì)列,Redis 單線程對(duì)該事件隊(duì)列不斷進(jìn)行處理。 這樣一來(lái),Redis 無(wú)需一直輪詢是否有請(qǐng)求實(shí)際發(fā)生,這就可以避免造成 CPU 資源浪費(fèi)。同時(shí),Redis 在對(duì)事件隊(duì)列中的事件進(jìn)行處理時(shí),會(huì)調(diào)用相應(yīng)的處理函數(shù),這就實(shí)現(xiàn)了基于事件的回調(diào)。 05 小結(jié) Redis之所以是性能王者,來(lái)自于使用了“物理優(yōu)勢(shì)”的內(nèi)存、極致的數(shù)據(jù)結(jié)構(gòu)和高效的IO模型。 IO模型的高效,是因?yàn)樗捎昧藘?nèi)核的epoll機(jī)制,使得Redis無(wú)需“關(guān)注”IO等待,可以持續(xù)不斷地對(duì)事件隊(duì)列進(jìn)行處理,所以能及時(shí)迅速地響應(yīng)客戶端請(qǐng)求,達(dá)到性能最優(yōu)。 閱讀原文:原文鏈接 該文章在 2025/8/25 13:07:18 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |