Redis基礎篇(二)高性能IO模型

咱們常常聽到說Redis是單線程的,也會有疑問:爲何單線程的Redis能那麼快?網絡

這裏要明白一點:Redis是單線程,主要是指Redis的網絡IO和鍵值對讀寫是由一個線程來完成的,這也是Redis對外提供鍵值存儲服務的主要流程。但Redis的其餘功能,好比持久化、異步刪除、集羣數據同步等,都是由額外的線程執行的。數據結構

咱們知道多線程可以提高併發性能,那爲何Redis會採用單線程,而非多線程?爲何單線程能那麼快?多線程

下面咱們就來學習一下Redis採用單線程的緣由。併發

爲何採用單線程?

使用多線程,雖然能夠增長系統吞吐率,或是增長系統擴展性,但一樣會產生開銷。異步

Redis的數據是在內存裏的,是共享的,若是使用多線程就會引起共享資源的競爭,須要引入互斥鎖來解決,使得並行變串行。最終系統吞吐率並無隨着線程的增長而增長。socket

另外,多線程開發須要精細的設計,會增長系統的複雜度,下降代碼的易調試性和可維護性。爲了不這些問題,Redis採用單線程模式。函數

單線程Redis爲何那麼快?

一般來講,單線程的處理能力比多線程要差不少,那Redis卻能使用單線程模型達到每秒數十萬級別的處理能力,這是爲何呢?高併發

一方面,Redis大多數操做是在內存上完成的,而且採用高效的數據結構,例如哈希表和跳錶。另外一方面,Redis採用了多路複用機制,使其在網絡IO操做中能併發處理大量的客戶端請求,實現高吞吐率。性能

在學習多路複用機制前,咱們要弄明白網絡操做的基於IO模型和潛在的阻塞點。學習

基本IO模型與阻塞點

以Get請求爲例,爲了處理一個Get請求:

  1. 須要監聽客戶端請求(bind/listen)
  2. 和客戶端創建鏈接(accept)
  3. 從socket中讀取請求(recv)
  4. 解析客戶端發送請求(parse)
  5. 根據請求類型讀取鍵值數據(get)
  6. 最後給客戶端返回結果,即向socket中寫回數據(send)。

下圖顯示了這一過程,其中,bind/listen、accept、recv、parse和send屬於網絡IO處理,而get屬性鍵值數據操做。

image

可是在這裏的網絡IO操做中,有潛在的阻塞點,分別是accept()和recv()。

  • 當Redis監聽到一個客戶端有鏈接請求,但一直未能成功創建起鏈接時,會阻塞在accept()
  • 當Redis經過recv()從一個客戶端讀取數據時,若是數據一直沒有到達,Redis也會一直阻塞在recv()

這就致使Redis整個線程阻塞,沒法處理其餘客戶端請求,效率很低。不過,幸運的是,socket網絡模型自己支持非阻塞模式。

非阻塞模式

Socket網絡模型能夠設置非阻塞模式。

image

這樣能保證Redis線程既不會像基本IO模型中一直在阻塞點等待,也不會致使Redis沒法處理實際到達的鏈接請求或數據。

下面就到多路複用機制登場了。

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

Linux的IO多路複用機制是指一個線程處理多個IO流,也就是select/epoll機制。

在Redis運行單線程下,該機制容許內核中,同時存在多個監聽套接字和已鏈接套接字。

基於多路複用的Redis IO模型

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

回調機制的工做流程:

  1. select/epoll一旦臨聽到FD上有請求到達,就會觸發相應的事件,並放進一個事件隊列中。
  2. Redis單線程對事件隊列進行處理便可,無需一直輪詢是否有請求發生,避免CPU資源浪費。

由於Redis一直在對事件隊列進行處理,因此能及時響應客戶端請求,提高Redis的響應性能。

不過,須要注意的是,在不一樣的操做系統上,多路複用機制也是適用的。

拓展

在「Redis基本IO模型」圖中,有哪些潛在的性能瓶頸?

Redis單線程處理IO請求性能瓶頸主要包括2個方面:

一、任意一個請求在server中一旦發生耗時,都會影響整個server的性能 也就是說後面的請求都要等前面這個耗時請求處理完成,本身才能被處理到。

耗時的操做包括:

  • 操做bigkey:寫入一個bigkey在分配內存時須要消耗更多的時間,一樣,刪除bigkey釋放內存一樣會產生耗時
  • 使用複雜度太高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,可是N很大,例如lrange key 0 -1一次查詢全量數據
  • 大量key集中過時:Redis的過時機制也是在主線程中執行的,大量key集中過時會致使處理一個請求時,耗時都在刪除過時key,耗時變長
  • 淘汰策略:溜達策略也是在主線程執行的,當內存超過Redis內存上限後,每次寫入都須要淘汰一些key,也會 形成耗時變長。
  • AOF刷盤開啓always機制:每次寫入都須要把這個操做刷到磁盤,寫磁盤的速度遠比寫內存慢,會拖慢Redis的性能
  • 主從全量同步生成RDB:雖然採用fork子進程生成數據快照,但fork這一瞬間也是會阻塞整個線程的,實例越大,阻塞時間越久

解決辦法:

  • 須要業務人員去規避
  • Redis在4.0推出了lazy-free機制,把bigkey釋放內存的耗時操做放在了異步線程中執行,下降對主線程的影響

二、併發量很是大時,單線程讀寫客戶端IO數據存在性能瓶頸,雖然採用IO多路複用機制,可是讀寫客戶端數據依舊是同步IO,只能單線程依次讀取客戶端的數據,沒法利用到CPU多核。

解決辦法:

  • Redis在6.0推出了多線程,能夠在高併發場景下利用CPU多核多線程讀寫客戶端數據,進一步提高server性能
  • 固然,只針對客戶端的讀寫是並行的,每一個命令的真正操做依舊是單線程的

參考資料

相關文章
相關標籤/搜索