天天看點

redis高性能IO模型:為什麼單線程Redis能那麼快?

今天,我們來探讨一個很多人都很關心的問題:“為什麼單線程的Redis能那麼快?”

首先,我要和你厘清一個事實,我們通常說,Redis是單線程,主要是指Redis的網絡IO和鍵值對讀寫是由一個線程來完成的,這也是Redis對外提供鍵值存儲服務的主要流程。但Redis的其他功能,比如持久化、異步删除、叢集資料同步等,其實是由額外的線程執行的。

是以,嚴格來說,Redis并不是單線程,但是我們一般把Redis稱為單線程高性能,這樣顯得“酷”些。接下來,我也會把Redis稱為單線程模式。而且,這也會促使你緊接着提問:“為什麼用單線程?為什麼單線程能這麼快?”

要弄明白這個問題,我們就要深入地學習下Redis的單線程設計機制以及多路複用機制。之後你在調優Redis性能時,也能更有針對性地避免會導緻Redis單線程阻塞的操作,例如執行複雜度高的指令。

好了,話不多說,接下來,我們就先來學習下Redis采用單線程的原因。

Redis為什麼用單線程?

要更好地了解Redis為什麼用單線程,我們就要先了解多線程的開銷。

多線程的開銷

日常寫程式時,我們經常會聽到一種說法:“使用多線程,可以增加系統吞吐率,或是可以增加系統擴充性。”的确,對于一個多線程的系統來說,在有合理的資源配置設定的情況下,可以增加系統中處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。下面的左圖是我們采用多線程時所期待的結果。

但是,請你注意,通常情況下,在我們采用多線程後,如果沒有良好的系統設計,實際得到的結果,其實是右圖所展示的那樣。我們剛開始增加線程數時,系統吞吐率會增加,但是,再進一步增加線程時,系統吞吐率就增長遲緩了,有時甚至還會出現下降的情況。

redis高性能IO模型:為什麼單線程Redis能那麼快?

為什麼會出現這種情況呢?一個關鍵的瓶頸在于,系統中通常會存在被多線程同時通路的共享資源,比如一個共享的資料結構。當有多個線程要修改這個共享資源時,為了保證共享資源的正确性,就需要有額外的機制進行保證,而這個額外的機制,就會帶來額外的開銷。

拿Redis來說,在上節課中,我提到過,Redis有List的資料類型,并提供出隊(LPOP)和入隊(LPUSH)操作。假設Redis采用多線程設計,如下圖所示,現在有兩個線程A和B,線程A對一個List做LPUSH操作,并對隊列長度加1。同時,線程B對該List執行LPOP操作,并對隊列長度減1。為了保證隊列長度的正确性,Redis需要讓線程A和B的LPUSH和LPOP串行執行,這樣一來,Redis可以無誤地記錄它們對List長度的修改。否則,我們可能就會得到錯誤的長度結果。這就是多線程程式設計模式面臨的共享資源的并發通路控制問題。

redis高性能IO模型:為什麼單線程Redis能那麼快?

并發通路控制一直是多線程開發中的一個難點問題,如果沒有精細的設計,比如說,隻是簡單地采用一個粗粒度互斥鎖,就會出現不理想的結果:即使增加了線程,大部分線程也在等待擷取通路共享資源的互斥鎖,并行變串行,系統吞吐率并沒有随着線程的增加而增加。

而且,采用多線程開發一般會引入同步原語來保護共享資源的并發通路,這也會降低系統代碼的易調試性和可維護性。為了避免這些問題,Redis直接采用了單線程模式。

講到這裡,你應該已經明白了“Redis為什麼用單線程”,那麼,接下來,我們就來看看,為什麼單線程Redis能獲得高性能。

單線程Redis為什麼那麼快?

通常來說,單線程的處理能力要比多線程差很多,但是Redis卻能使用單線程模型達到每秒數十萬級别的處理能力,這是為什麼呢?其實,這是Redis多方面設計選擇的一個綜合結果。

一方面,Redis的大部分操作在記憶體上完成,再加上它采用了高效的資料結構,例如哈希表和跳表,這是它實作高性能的一個重要原因。另一方面,就是Redis采用了多路複用機制,使其在網絡IO操作中能并發處理大量的用戶端請求,實作高吞吐率。接下來,我們就重點學習下多路複用機制。

首先,我們要弄明白網絡操作的基本IO模型和潛在的阻塞點。畢竟,Redis采用單線程進行IO,如果線程被阻塞了,就無法進行多路複用了。

基本IO模型與阻塞點

你還記得我在第一節課介紹的具有網絡架構的SimpleKV嗎?

以Get請求為例,SimpleKV為了處理一個Get請求,需要監聽用戶端請求(bind/listen),和用戶端建立連接配接(accept),從socket中讀取請求(recv),解析用戶端發送請求(parse),根據請求類型讀取鍵值資料(get),最後給用戶端傳回結果,即向socket中寫回資料(send)。

下圖顯示了這一過程,其中,bind/listen、accept、recv、parse和send屬于網絡IO處理,而get屬于鍵值資料操作。既然Redis是單線程,那麼,最基本的一種實作是在一個線程中依次執行上面說的這些操作。

redis高性能IO模型:為什麼單線程Redis能那麼快?

但是,在這裡的網絡IO操作中,有潛在的阻塞點,分别是accept()和recv()。當Redis監聽到一個用戶端有連接配接請求,但一直未能成功建立起連接配接時,會阻塞在accept()函數這裡,導緻其他用戶端無法和Redis建立連接配接。類似的,當Redis通過recv()從一個用戶端讀取資料時,如果資料一直沒有到達,Redis也會一直阻塞在recv()。

這就導緻Redis整個線程阻塞,無法處理其他用戶端請求,效率很低。不過,幸運的是,socket網絡模型本身支援非阻塞模式。

非阻塞模式

Socket網絡模型的非阻塞模式設定,主要展現在三個關鍵的函數調用上,如果想要使用socket非阻塞模式,就必須要了解這三個函數的調用傳回類型和設定模式。接下來,我們就重點學習下它們。

在socket模型中,不同操作調用後會傳回不同的套接字類型。socket()方法會傳回主動套接字,然後調用listen()方法,将主動套接字轉化為監聽套接字,此時,可以監聽來自用戶端的連接配接請求。最後,調用accept()方法接收到達的用戶端連接配接,并傳回已連接配接套接字。

redis高性能IO模型:為什麼單線程Redis能那麼快?

針對監聽套接字,我們可以設定非阻塞模式:當Redis調用accept()但一直未有連接配接請求到達時,Redis線程可以傳回處理其他操作,而不用一直等待。但是,你要注意的是,調用accept()時,已經存在監聽套接字了。

雖然Redis線程可以不用繼續等待,但是總得有機制繼續在監聽套接字上等待後續連接配接請求,并在有請求時通知Redis。

類似的,我們也可以針對已連接配接套接字設定非阻塞模式:Redis調用recv()後,如果已連接配接套接字上一直沒有資料到達,Redis線程同樣可以傳回處理其他操作。我們也需要有機制繼續監聽該已連接配接套接字,并在有資料達到時通知Redis。

這樣才能保證Redis線程,既不會像基本IO模型中一直在阻塞點等待,也不會導緻Redis無法處理實際到達的連接配接請求或資料。

到此,Linux中的IO多路複用機制就要登場了。

基于多路複用的高性能I/O模型

Linux中的IO多路複用機制是指一個線程處理多個IO流,就是我們經常聽到的select/epoll機制。簡單來說,在Redis隻運作單線程的情況下,該機制允許核心中,同時存在多個監聽套接字和已連接配接套接字。核心會一直監聽這些套接字上的連接配接請求或資料請求。一旦有請求到達,就會交給Redis線程處理,這就實作了一個Redis線程處理多個IO流的效果。

下圖就是基于多路複用的Redis IO模型。圖中的多個FD就是剛才所說的多個套接字。Redis網絡架構調用epoll機制,讓核心監聽這些套接字。此時,Redis線程不會阻塞在某一個特定的監聽或已連接配接套接字上,也就是說,不會阻塞在某一個特定的用戶端請求處理上。正因為此,Redis可以同時和多個用戶端連接配接并處理請求,進而提升并發性。

redis高性能IO模型:為什麼單線程Redis能那麼快?

為了在請求到達時能通知到Redis線程,select/epoll提供了基于事件的回調機制,即針對不同僚件的發生,調用相應的處理函數。

那麼,回調機制是怎麼工作的呢?其實,select/epoll一旦監測到FD上有請求到達時,就會觸發相應的事件。

這些事件會被放進一個事件隊列,Redis單線程對該事件隊列不斷進行處理。這樣一來,Redis無需一直輪詢是否有請求實際發生,這就可以避免造成CPU資源浪費。同時,Redis在對事件隊列中的事件進行處理時,會調用相應的處理函數,這就實作了基于事件的回調。因為Redis一直在對事件隊列進行處理,是以能及時響應用戶端請求,提升Redis的響應性能。

為了友善你了解,我再以連接配接請求和讀資料請求為例,具體解釋一下。

這兩個請求分别對應Accept事件和Read事件,Redis分别對這兩個事件注冊accept和get回調函數。當Linux核心監聽到有連接配接請求或讀資料請求時,就會觸發Accept事件和Read事件,此時,核心就會回調Redis相應的accept和get函數進行處理。

這就像病人去醫院瞧病。在醫生實際診斷前,每個病人(等同于請求)都需要先分診、測體溫、登記等。如果這些工作都由醫生來完成,醫生的工作效率就會很低。是以,醫院都設定了分診台,分診台會一直處理這些診斷前的工作(類似于Linux核心監聽請求),然後再轉交給醫生做實際診斷。這樣即使一個醫生(相當于Redis單線程),效率也能提升。

不過,需要注意的是,即使你的應用場景中部署了不同的作業系統,多路複用機制也是适用的。因為這個機制的實作有很多種,既有基于Linux系統下的select和epoll實作,也有基于FreeBSD的kqueue實作,以及基于Solaris的evport實作,這樣,你可以根據Redis實際運作的作業系統,選擇相應的多路複用實作。

小結

今天,我們重點學習了Redis線程的三個問題:“Redis真的隻有單線程嗎?”“為什麼用單線程?”“單線程為什麼這麼快?”