我們通常說,Redis 是單線程,主要是指 Redis 的網絡 IO 和鍵值對讀寫是由一個線程來完成的,這也是 Redis 對外提供鍵值存儲服務的主要流程。但 Redis 的其他功能,比如持久化、異步刪除、集群數據同步等,其實是由額外的線程執行的。
Redis 為什么用單線程?
日常寫程序時,我們經常會聽到一種說法:“使用多線程,可以增加系統吞吐率,或是可以增加系統擴展性。”的確,對于一個多線程的系統來說,在有合理的資源分配的情況下,可以增加系統中處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。
但是,通常情況下,在我們采用多線程后,如果沒有良好的系統設計,實際得到的結果,其實是右圖所展示的那樣。我們剛開始增加線程數時,系統吞吐率會增加,但是,再進一步增加線程時,系統吞吐率就增長遲緩了,有時甚至還會出現下降的情況。一個關鍵的原因在于,系統中通常會存在被多線程同時訪問的共享資源,比如一個共享的數據結構。當有多個線程要修改這個共享資源時,為了保證共享資源的正確性,就需要有額外的機制進行保證,而這個額外的機制,就會帶來額外的開銷。
并發訪問控制一直是多線程開發中的一個難點問題,如果沒有精細的設計,比如說,只是簡單地采用一個粗粒度互斥鎖,就會出現不理想的結果:即使增加了線程,大部分線程也在等待獲取訪問共享資源的互斥鎖,并行變串行,系統吞吐率并沒有隨著線程的增加而增加。而且,采用多線程開發一般會引入同步原語來保護共享資源的并發訪問,這也會降低系統代碼的易調試性和可維護性。為了避免這些問題,Redis 直接采用了單線程模式。
單線程 Redis 為什么那么快?
通常來說,單線程的處理能力要比多線程差很多,但是 Redis 卻能使用單線程模型達到每秒數十萬級別的處理能力,這是為什么呢?其實,這是 Redis 多方面設計選擇的一個綜合結果。
一方面,Redis 的大部分操作在內存上完成,再加上它采用了高效的數據結構,例如哈希表和跳表,這是它實現高性能的一個重要原因。
另一方面,就是 Redis 采用了多路復用機制,使其在網絡 IO 操作中能并發處理大量的客戶端請求,實現高吞吐率。
Redis單線程處理IO請求有哪些性能瓶頸?
一方面,任意一個請求在server中一旦發生耗時,都會阻塞后續請求的處理,從而影響整個server的性能。其中,耗時的操作包括如下:
- 操作bigkey:寫入一個bigkey在分配內存時需要消耗更多的時間,同樣,刪除bigkey釋放內存同樣會產生耗時;
- 使用復雜度過高的命令:例如SORT/SUNION/ZUNIONSTORE,或者N值很大的O(N)命令,例如lrange key 0 -1一次查詢全量數據;
- 大量key集中過期:Redis的過期機制也是在主線程中執行的,大量key集中過期會導致處理一個請求時,耗時都在刪除過期key,耗時變長;
- 淘汰策略:淘汰策略也是在主線程執行的,當內存超過Redis內存上限后,每次寫入都需要淘汰一些key,也會造成耗時變長;
- AOF刷盤開啟always機制:每次寫入都需要把這個操作刷到磁盤,寫磁盤的速度遠比寫內存慢,會拖慢Redis的性能;
- 主從全量同步生成RDB:雖然采用fork子進程生成數據快照,但fork這一瞬間也是會阻塞整個線程的,實例越大,阻塞時間越久;
另一方面,并發量非常大時,單線程讀寫客戶端IO數據存在性能瓶頸,雖然采用IO多路復用機制,但是讀寫客戶端數據依舊是同步IO,只能單線程依次讀取客戶端的數據,無法利用到CPU多核。