Redis是單線程,主要是指Redis的網絡IO和鍵值對讀寫是由一個線程來完成的,這也是Redis對外提供鍵值存儲服務的主要流程。但Redis的其餘功能,好比持久化、異步刪除、集羣數據同步等,實際上是由額外的線程執行的。編程
平常寫程序時,咱們常常會聽到一種說法:「使用多線程,能夠增長系統吞吐率,或是能夠增長系統擴展性。」的確,對於一個多線程的系統來講,在有合理的資源分配的狀況下,能夠增長系統中處理請求操做的資源實體,進而提高系統可以同時處理的請求數,即吞吐率。下面的左圖是咱們採用多線程時所期待的結果。markdown
可是,請你注意,一般狀況下,在咱們採用多線程後,若是沒有良好的系統設計,實際獲得的結果,實際上是右圖所展現的那樣。咱們剛開始增長線程數時,系統吞吐率會增長,可是,再進一步增長線程時,系統吞吐率就增加遲緩了,有時甚至還會出現降低的狀況。網絡
爲何會出現這種狀況呢?一個關鍵的瓶頸在於,系統中一般會存在被多線程同時訪問的共享資源,好比一個共享的數據結構。當有多個線程要修改這個共享資源時,爲了保證共享資源的正確性,就須要有額外的機制進行保證,而這個額外的機制,就會帶來額外的開銷。數據結構
拿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的大部分操做在內存上完成,再加上它採用了高效的數據結構,例如哈希表和跳錶,這是它實現高性能的一個重要緣由。另外一方面,就是Redis採用了多路複用機制,使其在網絡IO操做中能併發處理大量的客戶端請求,實現高吞吐率。接下來,咱們就重點學習下多路複用機制。 首先,咱們要弄明白網絡操做的基本IO模型和潛在的阻塞點。畢竟,Redis採用單線程進行IO,若是線程被阻塞了,就沒法進行多路複用了。socket
以Get請求爲例,爲了處理一個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網絡模型的非阻塞模式設置,主要體如今三個關鍵的函數調用上,若是想要使用socket非阻塞模式,就必需要了解這三個函數的調用返回類型和設置模式。接下來,咱們就重點學習下它們。
在socket模型中,不一樣操做調用後會返回不一樣的套接字(看作是不一樣主機之間的進程進行雙向通訊的端點,簡單的說就是通訊的兩方的一種約定,用套接字中的相關函數來完成通訊過程。)類型。socket()方法會返回主動套接字,而後調用listen()方法,將主動套接字轉化爲監聽套接字,此時,能夠監聽來自客戶端的鏈接請求。最後,調用accept()方法接收到達的客戶端鏈接,並返回已鏈接套接字。
針對監聽套接字,咱們能夠設置非阻塞模式:當Redis調用accept()但一直未有鏈接請求到達時,Redis線程能夠返回處理其餘操做,而不用一直等待。可是,你要注意的是,調用accept()時,已經存在監聽套接字了。
雖然Redis線程能夠不用繼續等待,可是總得有機制繼續在監聽套接字上等待後續鏈接請求,並在有請求時通知Redis。
相似的,咱們也能夠針對已鏈接套接字設置非阻塞模式:Redis調用recv()後,若是已鏈接套接字上一直沒有數據到達,Redis線程一樣能夠返回處理其餘操做。咱們也須要有機制繼續監聽該已鏈接套接字,並在有數據達到時通知Redis。
這樣才能保證Redis線程,既不會像基本IO模型中一直在阻塞點等待,也不會致使Redis沒法處理實際到達的鏈接請求或數據。
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函數進行處理。
這就像病人去醫院瞧病。在醫生實際診斷前,每一個病人(等同於請求)都須要先分診、測體溫、登記等。若是這些工做都由醫生來完成,醫生的工做效率就會很低。因此,醫院都設置了分診臺,分診臺會一直處理這些診斷前的工做(相似於Linux內核監聽請求),而後再轉交給醫生作實際診斷。這樣即便一個醫生(至關於Redis單線程),效率也能提高。
不過,須要注意的是,即便你的應用場景中部署了不一樣的操做系統,多路複用機制也是適用的。由於這個機制的實現有不少種,既有基於Linux系統下的select和epoll實現,也有基於FreeBSD的kqueue實現,以及基於Solaris的evport實現,這樣,你能夠根據Redis實際運行的操做系統,選擇相應的多路複用實現。