1. 程式人生 > 其它 >為什麼單執行緒Redis能那麼快?

為什麼單執行緒Redis能那麼快?

Redis單執行緒

  我們通常說,Redis是單執行緒,主要是指Redis的網路I/O和鍵值讀寫的是由一個執行緒來完成的。其他資料持久化、叢集資料同步、非同步刪除等,其實是由額外執行緒來完成的。

  所以,嚴格來說,Redis 並不是單執行緒,但是我們一般把 Redis 稱為單執行緒高效能,這樣顯得“酷”些。接下來,會把 Redis 稱為單執行緒模式。而且,這也會促使我們緊接著提問:“為什麼用單執行緒?為什麼單執行緒能這麼快?

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

Redis 為什麼用單執行緒?

  要更好地理解 Redis 為什麼用單執行緒,我們就要先了解多執行緒的開銷。

多執行緒的開銷

  在日常寫程式時,我們通常回聽到使用多執行緒會增加系統的吞吐率,或者可以增加系統的擴充套件性。確實對於一個多執行緒的系統,在合理的分配系統資源的情況下,可以增加系統處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。以下左圖是我們期望的效果。

  但是,在通常情況下,在我們採用多執行緒後,如果沒有精細的系統設計,實際得到的效果卻不盡人意。在開始增加執行緒數的時候,系統的吞吐率會提升,在進一步增加執行緒數的時候,系統的吞吐率提升就緩慢了,有時甚至出現下降的情況。

  為什麼會出現這個問題,因為在多執行緒的系統中,當出現多個執行緒同時需要訪問共享資源的時候,比如一個共享的資料結構。當有多個執行緒需要修改這個共享資源時,為了保證共享資源的正確性,就需要額外的機制去保證,而這個額外的機制就會帶來額外的系統開銷。多執行緒會面臨共享資源的併發訪問控制問題

單執行緒 Redis 為什麼那麼快?

  通常來說,單執行緒的處理能力要比多執行緒差得多,但是Redis為什麼能夠使用單執行緒達到每秒數十萬級別的處理能力呢?其實,時Redis多方面設計的一個綜合結果。

  一方面,Redis的操作都在記憶體中完成,再加上它採用了高效的資料結構,例如雜湊表和跳錶,這是它實現高效能的一個重要原因。另一方面,Redis使用了多路複用機制

,使其在網路的IO的時候能夠併發處理大量的客戶端請求,實現高吞吐率。我們來學習下多路複用機制。

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

基本 IO 模型與阻塞點

  這裡以具有網路框架的簡單鍵值庫為例說明。

  以Get請求為例,為了處理一個Get請求,需要先監聽客戶端請求(bind/listen),建立客戶端連線(accept),解析客戶端請求(parse),根據請求模型獲取鍵值資料(get),最後給客戶端返回結果,即向socket中寫回資料(send)。

  下圖展示了這一過程,其中,bind/listen、accept、parse和send都屬於網路I/O處理,而get資料鍵值資料操作。既然Redis是單執行緒,最基本的一種實現是在一個執行緒中依次執行上面說的這些操作。

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

  這就導致 Redis 整個執行緒阻塞,無法處理其他客戶端請求,效率很低。不過,幸運的是,socket 網路模型本身支援非阻塞模式。

非阻塞模式

  Socket 網路模型的非阻塞模式設定,主要體現在三個關鍵的函式呼叫上,如果想要使用 socket 非阻塞模式,就必須要了解這三個函式的呼叫返回型別和設定模式。接下來,我們就重點學習下它們。

  在Socket模型中,不同的操作回撥用後回返回不同的套接字型別。socket()方法會返回主動套接字,然後返回listen()方法,將主動套接字轉化為監聽套接字,此時,可以監聽來自客戶端的連線請求。最後,呼叫accept()方法接收到達的客戶端連線,並返回已連線套接字。

  針對監聽套接字,我們可以設定非阻塞模式:當Redis呼叫accpet()但一直未有連線請求到達時,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 執行緒,select/epoll 提供了基於事件的回撥機制,即針對不同事件的發生,呼叫相應的處理函式

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

  這些事件會被放入到一個事件佇列,Redis單執行緒對該事件佇列不斷進行處理。這樣依賴,Redis無需一直輪詢是否有請求實際發生,這就可以避免造成CPU資源浪費。同時,Redis在對事件佇列中的事件進行處理時,會呼叫相應的處理函式,這就實現了基於事件的回撥。因為Redis一直在對事件佇列進行處理,所以能及時相應客戶端請求,提升Redis的響應效能。

  為了方便理解,以連線請求和讀資料請求為例,解釋:

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

  

小結

今天,本次整理了 Redis 執行緒的三個問題:“Redis 真的只有單執行緒嗎?”“為什麼用單執行緒?”“單執行緒為什麼這麼快?

  1、Redis的單執行緒是指它對網路IO和資料讀寫的操作採用了一個執行緒。

  2、採用單執行緒的核心原因就是避免多執行緒開發的併發控制問題。

  3、單執行緒的Redis也能獲得高效能,跟多路複用的 IO 模型密切相關,因為這避免了 accept() 和 send()/recv() 潛在的網路 IO 操作阻塞點。