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

2022-09-13 07:57:08 字數 4435 閱讀 6747

首先,我們說一下為什麼 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 直接採用了單執行緒模式。

通常來說,單執行緒的處理能力要比多執行緒差很多,但是 redis 卻能使用單執行緒模型達到每秒數十萬級別的處理能力,這是為什麼呢?

一方面,redis 的大部分操作在記憶體上完成,再加上它採用了高效的資料結構,例如雜湊表和跳表,這是它實現高效能的乙個重要原因。另一方面,就是 redis 採用了多路復用機制,使其在網路 io 操作中能併發處理大量的客戶端請求,實現高吞吐率。

下面我們講一下多路復用機制,首先,我們要弄明白網路操作的基本 io 模型和潛在的阻塞點。畢竟,redis 採用單執行緒進行 io,如果執行緒被阻塞了,就無法進行多路復用了。

假設 redis 採用基本的 io 模型,以 get 請求為例,那麼 redis 為了處理乙個 get 請求,需要監聽客戶端請求(bind/listen),和客戶端建立連線(accept),從 socket 中讀取請求(recv),解析客戶端傳送請求(parse),根據請求型別讀取鍵值資料(get),最後給客戶端返回結果,即向 socket 中寫回資料(send)。

其中,bind/listen、accept、recv、parse 和 send 屬於網路 io 處理,而 get 屬於鍵值資料操作。既然 redis 是單執行緒,那麼,最基本的一種實現是在乙個執行緒中依次執行上面說的這些操作。如下圖所示:

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

這就導致 redis 整個執行緒阻塞,無法處理其他客戶端請求,效率很低。

socket 網路模型的非阻塞模式設定,主要體現在三個關鍵的函式呼叫上,socket(),listen(),accept()。

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

呼叫方法

返回套接字型別

非阻塞模式

效果socket()

主動套接字

listen()

監聽套接字

可設定accept()非阻塞

accept()

已連線套接字

可設定send()/ revc()非阻塞

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

雖然 redis 執行緒可以不用繼續等待,但是總得有機制繼續在監聽套接字上等待後續連線請求,並在有請求時通知 redis。

類似的,我們也可以針對已連線套接字設定非阻塞模式:redis 呼叫 recv() 後,如果已連線套接字上一直沒有資料到達,redis 執行緒同樣可以返回處理其他操作。我們也需要有機制繼續監聽該已連線套接字,並在有資料達到時通知 redis。這樣才能保證 redis 執行緒,既不會像基本 io 模型中一直在阻塞點等待,也不會導致 redis 無法處理實際到達的連線請求或資料。

linux 中的 io 多路復用機制是指乙個執行緒處理多個 io 流,就是我們經常聽到的 select/epoll 機制。關於這個機制,可以看看我之前的文章。簡單來說,在 redis 只執行單執行緒的情況下,該機制允許核心中,同時存在多個監聽套接字和已連線套接字。核心會一直監聽這些套接字上的連線請求或資料請求。一旦有請求到達,就會交給 redis 執行緒處理,這就實現了乙個 redis 執行緒處理多個 io 流的效果。

下圖就是基於多路復用的 redis io 模型。圖中的多個 fd 就是剛才所說的多個套接字。redis 網路框架呼叫 epoll 機制(假設 redis 安裝在 linux 系統中),讓核心監聽這些套接字。此時,redis 執行緒不會阻塞在某乙個特定的監聽或已連線套接字上,也就是說,不會阻塞在某乙個特定的客戶端請求處理上。正因為此,redis 可以同時和多個客戶端連線並處理請求,從而提公升併發性。

為了在請求到達時能通知到 redis 執行緒,select/epoll 提供了基於事件的**機制,即針對不同事件的發生,呼叫相應的處理函式。

那麼,**機制是怎麼工作的呢?其實,select/epoll 一旦監測到 fd 上有請求到達時,就會觸發相應的事件。這些事件會被放進乙個事件佇列,redis 單執行緒對該事件佇列不斷進行處理。這樣一來,redis 無需一直輪詢是否有請求實際發生,這就可以避免造成 cpu 資源浪費。同時,redis 在對事件佇列中的事件進行處理時,會呼叫相應的處理函式,這就實現了基於事件的**。因為 redis 一直在對事件佇列進行處理,所以能及時響應客戶端請求,提公升 redis 的響應效能。

注意:不同作業系統中的io多路復用機制有不同的實現,比如基於linux 系統下的 select 和 epoll 實現,也有基於 freebsd 的 kqueue 實現,以及基於 solaris 的 evport 實現。

redis單執行緒處理io請求效能瓶頸主要包括2個方面:

1、任意乙個請求在server中一旦發生耗時,都會影響整個server的效能,也就是說後面的請求都要等前面這個耗時請求處理完成,自己才能被處理到。耗時的操作包括以下幾種:

a、操作bigkey:寫入乙個bigkey在分配記憶體時需要消耗更多的時間,同樣,刪除bigkey釋放記憶體同樣會產生耗時;

b、使用複雜度過高的命令:例如sort/sunion/zunionstore,或者o(n)命令,但是n很大,例如lrange key 0 -1一次查詢全量資料;

c、大量key集中過期:redis的過期機制也是在主線程中執行的,大量key集中過期會導致處理乙個請求時,耗時都在刪除過期key,耗時變長;

d、淘汰策略:淘汰策略也是在主線程執行的,當記憶體超過redis記憶體上限後,每次寫入都需要淘汰一些key,也會造成耗時變長;

e、aof刷盤開啟always機制:每次寫入都需要把這個操作刷到磁碟,寫磁碟的速度遠比寫記憶體慢,會拖慢redis的效能;

f、主從全量同步生成rdb:雖然採用fork子程序生成資料快照,但fork這一瞬間也是會阻塞整個執行緒的,例項越大,阻塞時間越久;

2、併發量非常大時,單執行緒讀寫客戶端io資料存在效能瓶頸,雖然採用io多路復用機制,但是讀寫客戶端資料依舊是同步io,只能單執行緒依次讀取客戶端的資料,無法利用到cpu多核。

好啦,到此我們的 redis 為啥這麼快就講完了。

巨人的肩膀:

Redis為什麼是單執行緒

經過多方資料收集 總結 思考,結論如下 準確地來說,該問題是 為什麼redis採用單程序單執行緒模型 我們從兩個層次去理解 第乙個層次 我們多執行緒的使用情景是io密集型,目的是為了充分利用cpu資源。也就是說當乙個執行緒io等待的時候,另乙個執行緒可以進行執行,達到充分利用cpu資源的效果,不要讓...

Redis為什麼單執行緒還那麼快?執行緒安全嗎?

redis是單執行緒,執行緒安全的 1 絕大部分請求是純粹的記憶體操作 非常快速 2 採用單執行緒,避免了不必要的上下文切換和競爭條件 3 非阻塞io io多路復用 io多路復用中有三種方式 select,poll,epoll。需要注意的是,select,poll是執行緒不安全的,epoll是執行緒...

Redis為什麼是單執行緒的

因為redis是基於記憶體的操作,cpu不是redis的瓶頸,redis的瓶頸最有可能是機器記憶體的大小或者網路頻寬。既然單執行緒容易實現,而且cpu不會成為瓶頸,那就順理成章地採用單執行緒的方案了。二 詳細原因 1 不需要各種鎖的效能消耗 redis的資料結構並不全是簡單的key value,還有...