既然Redis是單線程的工作模式,如何處理那麼多的并發用戶端連接配接?
Redis 是個單線程程式!這點必須銘記。也許你會懷疑高并發的 Redis 中間件怎麼可能是單線程。很抱歉,它就是單線程,你的懷疑暴露了你基礎知識的不足。莫要瞧不起單線程,除了 Redis 之外,Node.js 也是單線程,Nginx 也是單線程,但是它們都是伺服器高性能的典範。
優勢使用不當就會變成劣勢。redis單線程很快,因為它所有的資料都在記憶體中,所有的運算都是記憶體級别的運算。正因為 Redis 是單線程,是以要小心使用 Redis 指令,對于那些時間複雜度為 O(n) 級别的指令,一定要謹慎使用,一不小心就可能會導緻 Redis 卡頓。
要了解它的高并發,首先要非阻塞 IO,多路複用這些詞彙。
阻塞IO | 非阻塞IO |
當我們調用套接字的讀寫方法,預設它們是阻塞的。當使用者線程發出IO請求之後,核心會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者線程就會處于阻塞狀态,使用者線程交出CPU。當資料就緒之後,核心會将資料拷貝到使用者線程,并傳回結果給使用者線程,使用者線程才解除block狀态。 | 使用者線程需要不斷地詢問核心資料是否就緒,也就說非阻塞IO不會交出CPU,而會一直占用CPU。 非阻塞 IO 在套接字對象上提供了一個選項 ,當這個選項打開時,讀寫方法不會阻塞,而是能讀多少讀多少,能寫多少寫多少。能讀/寫多少取決于核心為套接字配置設定的讀/寫緩沖區内部的資料位元組數,讀方法和寫方法都會通過傳回值來告知程式實際讀寫了多少位元組。 |
事件輪詢(多路複用)
Q1:誰去輪訓的詢問socket狀态的問題,使用者線程效率不高
Q2:使用者線程該讀/寫多少何時才應該繼續(線程如何得到通知)
事件輪詢 API(它是Java 語言裡面的 NIO 技術) 就是用來解決這2類問題的,首先它由核心輪訓取代了使用者線程輪訓,并由核心通知,最簡單的事件輪詢 API 是
函數,它是作業系統提供給使用者程式的 API。輸入是讀寫描述符清單
select
,輸出是與之對應的可讀可寫事件。同時還提供了一個
read_fds & write_fds
參數,如果沒有任何事件到來,那麼就最多等待
timeout
時間,線程處于阻塞狀态。一旦期間有任何事件到來,就可以立即傳回。時間過了之後還是沒有任何事件到來,也會立即傳回。拿到事件後,線程就可以繼續挨個處理相應的事件。處理完了繼續過來輪詢。于是線程就進入了一個死循環,我們把這個死循環稱為事件循環,一個循環為一個周期。
timeout
因為我們通過![]()
redis線程模型_Redis系列四Redis的線程 IO 模型 系統調用同時處理多個通道描述符的讀寫事件,是以我們将這類系統調用稱為多路複用 API。現代作業系統的多路複用 API 已經不再使用
select
系統調用,而改用
select
和
epoll(linux)
kqueue(freebsd & macosx)
,因為 select 系統調用的性能在描述符特别多時性能會非常差。它們使用起來可能在形式上略有差異,但是本質上都是差不多的,都可以使用下面面的僞代碼邏輯進行了解。
while(true){
read_events, write_events = select(read_fds, write_fds, timeout)
for event in read_events:
handle_read(event.fd)
for event in write_events:
handle_write(event.fd)
handle_others() # 處理其它事情,如定時任務等
}
伺服器套接字
對象的讀操作是指調用
serversocket
接受用戶端新連接配接。何時有新連接配接到來,也是通過
accept
系統調用的讀事件來得到通知的。
select
服務端
redis server就是通過blocking_keys(指令隊列)和ready_keys(響應隊列)兩個資料結構來實作的阻塞操作。但整個阻塞并沒有阻塞EventLoop本身,進而實作指令的快速響應。算是一個典型的空間換時間的設計思路。
指令隊列
Redis 會将每個用戶端套接字都關聯一個指令隊列。用戶端的指令通過隊列來排隊進行順序處理,先到先服務。
響應隊列
Redis 同樣也會為每個用戶端套接字關聯一個響應隊列。Redis 伺服器通過響應隊列來将指令的傳回結果回複給用戶端。如果隊列為空,那麼意味着連接配接暫時處于空閑狀态,不需要去擷取寫事件,也就是可以将目前的用戶端描述符從
write_fds
裡面移出來。等到隊列有資料了,再将描述符放進去。避免
select
系統調用立即傳回寫事件,結果發現沒什麼資料可以寫。出這種情況的線程會飙高 CPU。
定時任務
伺服器處理要響應 IO 事件外,還要處理其它事情。比如定時任務就是非常重要的一件事。如果線程阻塞在 select 系統調用上,定時任務将無法得到準時排程。那 Redis 是如何解決這個問題的呢?Redis 的定時任務會記錄在一個稱為
最小堆
的資料結構中。這個堆中,最快要執行的任務排在堆的最上方。在每個循環周期,Redis 都會将最小堆裡面已經到點的任務立即進行處理。處理完畢後,将最快要執行的任務還需要的時間記錄下來,這個時間就是
select
系統調用的
timeout
參數。因為 Redis 知道未來
timeout
時間内,沒有其它定時任務需要處理,是以可以安心睡眠
timeout
的時間。
用戶端
分兩種I/O模型來闡述:阻塞I/O(BIO)和非阻塞I/O(NIO),Jedis是BIO。
BinaryJedis 中一個blpop請求會向redis發起兩次I/O請求,一次向redis發送BLPOP key指令,一次從對應的連結管道(channel)中讀取資料。由于BIO的特性當channel中沒有資料時會一直阻塞,直到有新資料為止。這樣就實作了用戶端的阻塞效果。注意:這裡的連結是被獨享的,不然會有資料幹擾。