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 操作阻塞點。
