Redis為什么那么快

為什么redis那么快?單線程的redis為什么那么快?
在學(xué)習(xí)使用redis時(shí),都會(huì)有這樣的疑慮。大家說(shuō)的redis是單線程的,其實(shí)指 Redis 的網(wǎng)絡(luò) IO 和鍵值對(duì)讀寫是由一個(gè)線程來(lái)完成的,這也是 Redis 對(duì)外提供鍵值存儲(chǔ)服務(wù)的主要流程。 不過Redis 的其他功能,比如持久化、異步刪除、集群數(shù)據(jù)同步等,其實(shí)是由額外的線程執(zhí)行的。

Redis 為什么用單線程

要更好地理解 Redis 為什么用單線程,我們就要先了解多線程的開銷。多線程的開銷日常寫程序時(shí),我們經(jīng)常會(huì)聽到一種說(shuō)法:“使用多線程,可以增加系統(tǒng)吞吐率,或是可以增加系統(tǒng)擴(kuò)展性?!钡拇_,對(duì)于一個(gè)多線程的系統(tǒng)來(lái)說(shuō),在有合理的資源分配的情況下,可以增加系統(tǒng)中處理請(qǐng)求操作的資源實(shí)體,進(jìn)而提升系統(tǒng)能夠同時(shí)處理的請(qǐng)求數(shù),即吞吐率。下面的左圖是我們采用多線程時(shí)所期待的結(jié)果。但是,請(qǐng)你注意,通常情況下,在我們采用多線程后,如果沒有良好的系統(tǒng)設(shè)計(jì),實(shí)際得到的結(jié)果,其實(shí)是下圖所展示的那樣。我們剛開始增加線程數(shù)時(shí),系統(tǒng)吞吐率會(huì)增加,但是,再進(jìn)一步增加線程時(shí),系統(tǒng)吞吐率就增長(zhǎng)遲緩了,有時(shí)甚至還會(huì)出現(xiàn)下降的情況。

image.png

為什么會(huì)出現(xiàn)這種情況呢?一個(gè)關(guān)鍵的瓶頸在于,系統(tǒng)中通常會(huì)存在被多線程同時(shí)訪問的共享資源,比如一個(gè)共享的數(shù)據(jù)結(jié)構(gòu)。當(dāng)有多個(gè)線程要修改這個(gè)共享資源時(shí),為了保證共享資源的正確性,就需要有額外的機(jī)制進(jìn)行保證,而這個(gè)額外的機(jī)制,就會(huì)帶來(lái)額外的開銷。拿 Redis 來(lái)說(shuō),在上節(jié)課中,我提到過,Redis 有 List 的數(shù)據(jù)類型,并提供出隊(duì)(LPOP)和入隊(duì)(LPUSH)操作。假設(shè) Redis 采用多線程設(shè)計(jì),如下圖所示,現(xiàn)在有兩個(gè)線程 A 和 B,線程 A 對(duì)一個(gè) List 做 LPUSH 操作,并對(duì)隊(duì)列長(zhǎng)度加 1。同時(shí),線程 B 對(duì)該 List 執(zhí)行 LPOP 操作,并對(duì)隊(duì)列長(zhǎng)度減 1。為了保證隊(duì)列長(zhǎng)度的正確性,Redis 需要讓線程 A 和 B 的 LPUSH 和 LPOP 串行執(zhí)行,這樣一來(lái),Redis 可以無(wú)誤地記錄它們對(duì) List 長(zhǎng)度的修改。否則,我們可能就會(huì)得到錯(cuò)誤的長(zhǎng)度結(jié)果。這就是多線程編程模式面臨的共享資源的并發(fā)訪問控制問題。
image.png

并發(fā)訪問控制一直是多線程開發(fā)中的一個(gè)難點(diǎn)問題,如果沒有精細(xì)的設(shè)計(jì),比如說(shuō),只是簡(jiǎn)單地采用一個(gè)粗粒度互斥鎖,就會(huì)出現(xiàn)不理想的結(jié)果:即使增加了線程,大部分線程也在等待獲取訪問共享資源的互斥鎖,并行變串行,系統(tǒng)吞吐率并沒有隨著線程的增加而增加。而且,采用多線程開發(fā)一般會(huì)引入同步原語(yǔ)來(lái)保護(hù)共享資源的并發(fā)訪問,這也會(huì)降低系統(tǒng)代碼的易調(diào)試性和可維護(hù)性。為了避免這些問題,Redis 直接采用了單線程模式。

單線程 Redis 為什么那么快?

單線程的處理能力要比多線程差很多,但是 Redis 卻能使用單線程模型達(dá)到每秒數(shù)十萬(wàn)級(jí)別的處理能力,這是為什么呢?其實(shí),這是 Redis 多方面設(shè)計(jì)選擇的一個(gè)綜合結(jié)果。
一方面,Redis 的大部分操作在內(nèi)存上完成,再加上它采用了高效的數(shù)據(jù)結(jié)構(gòu),例如哈希表和跳表,這是它實(shí)現(xiàn)高性能的一個(gè)重要原因。另一方面,就是 Redis 采用了多路復(fù)用機(jī)制,使其在網(wǎng)絡(luò) IO 操作中能并發(fā)處理大量的客戶端請(qǐng)求,實(shí)現(xiàn)高吞吐率。

基本 IO 模型與阻塞點(diǎn)

以 Get 請(qǐng)求為例,如果說(shuō)redis的單線程的,SimpleKV 為了處理一個(gè) Get 請(qǐng)求,需要監(jiān)聽客戶端請(qǐng)求(bind/listen),和客戶端建立連接(accept),從 socket 中讀取請(qǐng)求(recv),解析客戶端發(fā)送請(qǐng)求(parse),根據(jù)請(qǐng)求類型讀取鍵值數(shù)據(jù)(get),最后給客戶端返回結(jié)果,即向 socket 中寫回?cái)?shù)據(jù)(send)。下圖顯示了這一過程,其中,bind/listen、accept、recv、parse 和 send 屬于網(wǎng)絡(luò) IO 處理,而 get 屬于鍵值數(shù)據(jù)操作。既然 Redis 是單線程,那么,最基本的一種實(shí)現(xiàn)是在一個(gè)線程中依次執(zhí)行上面說(shuō)的這些操作。


image.png

但是,在這里的網(wǎng)絡(luò) IO 操作中,有潛在的阻塞點(diǎn),分別是 accept() 和 recv()。當(dāng) Redis 監(jiān)聽到一個(gè)客戶端有連接請(qǐng)求,但一直未能成功建立起連接時(shí),會(huì)阻塞在 accept() 函數(shù)這里,導(dǎo)致其他客戶端無(wú)法和 Redis 建立連接。類似的,當(dāng) Redis 通過 recv() 從一個(gè)客戶端讀取數(shù)據(jù)時(shí),如果數(shù)據(jù)一直沒有到達(dá),Redis 也會(huì)一直阻塞在 recv()。這就導(dǎo)致 Redis 整個(gè)線程阻塞,無(wú)法處理其他客戶端請(qǐng)求,效率很低。不過,幸運(yùn)的是,socket 網(wǎng)絡(luò)模型本身支持非阻塞模式。

非阻塞模式

Socket 網(wǎng)絡(luò)模型的非阻塞模式設(shè)置,主要體現(xiàn)在三個(gè)關(guān)鍵的函數(shù)調(diào)用上,如果想要使用 socket 非阻塞模式,就必須要了解這三個(gè)函數(shù)的調(diào)用返回類型和設(shè)置模式。接下來(lái),我們就重點(diǎn)學(xué)習(xí)下它們。在 socket 模型中,不同操作調(diào)用后會(huì)返回不同的套接字類型。socket() 方法會(huì)返回主動(dòng)套接字,然后調(diào)用 listen() 方法,將主動(dòng)套接字轉(zhuǎn)化為監(jiān)聽套接字,此時(shí),可以監(jiān)聽來(lái)自客戶端的連接請(qǐng)求。最后,調(diào)用 accept() 方法接收到達(dá)的客戶端連接,并返回已連接套接字。


image.png

針對(duì)監(jiān)聽套接字,我們可以設(shè)置非阻塞模式:當(dāng) Redis 調(diào)用 accept() 但一直未有連接請(qǐng)求到達(dá)時(shí),Redis 線程可以返回處理其他操作,而不用一直等待。但是,你要注意的是,調(diào)用 accept() 時(shí),已經(jīng)存在監(jiān)聽套接字了。雖然 Redis 線程可以不用繼續(xù)等待,但是總得有機(jī)制繼續(xù)在監(jiān)聽套接字上等待后續(xù)連接請(qǐng)求,并在有請(qǐng)求時(shí)通知 Redis。類似的,我們也可以針對(duì)已連接套接字設(shè)置非阻塞模式:Redis 調(diào)用 recv() 后,如果已連接套接字上一直沒有數(shù)據(jù)到達(dá),Redis 線程同樣可以返回處理其他操作。我們也需要有機(jī)制繼續(xù)監(jiān)聽該已連接套接字,并在有數(shù)據(jù)達(dá)到時(shí)通知 Redis。這樣才能保證 Redis 線程,既不會(huì)像基本 IO 模型中一直在阻塞點(diǎn)等待,也不會(huì)導(dǎo)致 Redis 無(wú)法處理實(shí)際到達(dá)的連接請(qǐng)求或數(shù)據(jù)。

基于多路復(fù)用的高性能 I/O 模型

Linux 中的 IO 多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè) IO 流,就是我們經(jīng)常聽到的 select/epoll 機(jī)制。簡(jiǎn)單來(lái)說(shuō),在 Redis 只運(yùn)行單線程的情況下,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽套接字和已連接套接字。內(nèi)核會(huì)一直監(jiān)聽這些套接字上的連接請(qǐng)求或數(shù)據(jù)請(qǐng)求。一旦有請(qǐng)求到達(dá),就會(huì)交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
下圖就是基于多路復(fù)用的 Redis IO 模型。圖中的多個(gè) FD 就是剛才所說(shuō)的多個(gè)套接字。Redis 網(wǎng)絡(luò)框架調(diào)用 epoll 機(jī)制,讓內(nèi)核監(jiān)聽這些套接字。此時(shí),Redis 線程不會(huì)阻塞在某一個(gè)特定的監(jiān)聽或已連接套接字上,也就是說(shuō),不會(huì)阻塞在某一個(gè)特定的客戶端請(qǐng)求處理上。正因?yàn)榇?,Redis 可以同時(shí)和多個(gè)客戶端連接并處理請(qǐng)求,從而提升并發(fā)性。

image.png

為了在請(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)。因?yàn)?Redis 一直在對(duì)事件隊(duì)列進(jìn)行處理,所以能及時(shí)響應(yīng)客戶端請(qǐng)求,提升 Redis 的響應(yīng)性能。

為了方便你理解,這里再以連接請(qǐng)求和讀數(shù)據(jù)請(qǐng)求為例,具體解釋一下。

這兩個(gè)請(qǐng)求分別對(duì)應(yīng) Accept 事件和 Read 事件,Redis 分別對(duì)這兩個(gè)事件注冊(cè) accept 和 get 回調(diào)函數(shù)。當(dāng) Linux 內(nèi)核監(jiān)聽到有連接請(qǐng)求或讀數(shù)據(jù)請(qǐng)求時(shí),就會(huì)觸發(fā) Accept 事件和 Read 事件,此時(shí),內(nèi)核就會(huì)回調(diào) Redis 相應(yīng)的 accept 和 get 函數(shù)進(jìn)行處理。

這就像病人去醫(yī)院瞧病。在醫(yī)生實(shí)際診斷前,每個(gè)病人(等同于請(qǐng)求)都需要先分診、測(cè)體溫、登記等。如果這些工作都由醫(yī)生來(lái)完成,醫(yī)生的工作效率就會(huì)很低。所以,醫(yī)院都設(shè)置了分診臺(tái),分診臺(tái)會(huì)一直處理這些診斷前的工作(類似于 Linux 內(nèi)核監(jiān)聽請(qǐng)求),然后再轉(zhuǎn)交給醫(yī)生做實(shí)際診斷。這樣即使一個(gè)醫(yī)生(相當(dāng)于 Redis 單線程),效率也能提升。

不過,需要注意的是,即使你的應(yīng)用場(chǎng)景中部署了不同的操作系統(tǒng),多路復(fù)用機(jī)制也是適用的。因?yàn)檫@個(gè)機(jī)制的實(shí)現(xiàn)有很多種,既有基于 Linux 系統(tǒng)下的 select 和 epoll 實(shí)現(xiàn),也有基于 FreeBSD 的 kqueue 實(shí)現(xiàn),以及基于 Solaris 的 evport 實(shí)現(xiàn),這樣,你可以根據(jù) Redis 實(shí)際運(yùn)行的操作系統(tǒng),選擇相應(yīng)的多路復(fù)用實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容