如果沒有良好的系統設計,採用多執行緒通常會導致如右圖所示的結果(注意縱座標)。剛開始增加執行緒數時,系統吞吐率會增加,再進一步增加執行緒時,系統吞吐率就成長遲緩了,甚至還會出現下降的情況。
關鍵瓶頸在於: 系統中通常會存在會被多執行緒同時存取的共享資源,為了確保共享資源的正確性,就需要有額外的機制保證線程安全性,例如加鎖,這會帶來額外的開銷。
例如拿最常用的List
類型來舉例吧,假設Redis採用多執行緒設計,有兩個執行緒A和B分別對List
做 LPUSH
和LPUSH
操作,為了使得每次執行都是相同的結果,即【B執行緒取出A執行緒放入的資料】就需要讓這兩個過程串列執行。這就是多執行緒程式設計模式面臨的共享資源的並發存取控制問題。
並發存取控制一直是多執行緒開發中的一個困難問題:如果只是簡單地採用一個互斥鎖,就會出現即使增加了線程,大部分線程也在等待取得互斥鎖,並行變串行,系統吞吐率並沒有隨著執行緒的增加而增加。
同時加入並發存取控制後也會降低系統程式碼的可讀性和可維護性,所以Redis乾脆直接採用了單執行緒模式。
之所以使用單執行緒是Redis設計者多面向測量的結果。
Redis的大部分操作在記憶體上完成
#採用了高效的資料結構,例如哈希表和跳表
採用了多路復用機制,使其在網路IO操作中能並發處理大量的客戶端請求,實現高吞吐率
既然Redis使用單執行緒進行IO,如果執行緒被阻塞了就無法進行多路復用了,所以不難想像,Redis肯定還針對網路和IO操作的潛在阻塞點進行了設計。
在網路通訊裡,伺服器為了處理一個Get請求,需要監聽客戶端請求(bind/listen
),和客戶端建立連線(accept
),從socket中讀取請求(recv
),解析客戶端發送請求(parse
),最後給客戶端傳回結果(send
)。
最基本的一種單執行緒實作是依序執行上面的操作。
上面標紅的accept和recv操作都是潛在的阻塞點:
當Redis監聽到有連接請求,但卻一直無法成功建立起連線時,就會阻塞在accept()
函數這裡,其他客戶端此時也無法和Redis建立連線
當Redis透過recv()
從一個客戶端讀取資料時,如果資料一直沒有到達,也會一直阻塞
select/epoll)。
accept和
get回呼函數。當Linux核心監聽到有連接請求或讀取資料請求時,就會觸發Accept事件和Read事件,此時,核心就會回呼Redis對應的
accept和
get函數進行處理。
如果任何一個請求在Redis中耗時較長,就會對整個伺服器的效能產生影響。後面的請求都要等前面這個耗時請求處理完成,自己才能被處理到。
這一點需要我們在設計業務場景時去規避;Redis的lazy-free
機制也把釋放記憶體的耗時操作放在了非同步執行緒中去執行了。
並發量非常大時,單執行緒讀寫客戶端IO資料存在效能瓶頸,雖然採用IO多路復用機制,但還是只能單執行緒依序讀取客戶端的數據,無法利用到CPU多核心。
Redis在6.0可以利用CPU多核心多執行緒讀寫客戶端數據,但只是針對客戶端的讀寫是並行的,每個命令的真正操作還是單線程。
藉此機會也提幾個和redis相關的有趣的問題。
為什麼要用Redis,直接存取記憶體不好嗎?
這一條其實並沒有很明確的界定,對於一些不常變動的數據,可以直接放到記憶體裡,不一定要放到Redis裡,可以放到記憶體裡。在更新資料時可能存在一致性問題,即可能只有某一台伺服器上的資料被修改,因此資料僅在本機記憶體中存在。存取Redis伺服器可以解決一致性問題,用Redis的話。
資料太多內存放不下怎麼辦?例如我要快取100G的數據,怎麼辦?
這裡也要打一個廣告Tair是淘寶開源的分散式KV快取系統,它從Redis繼承了豐富的操作,理論上總資料量無限制,針對可用性、可擴充性、可靠性也進行了升級,有興趣的夥伴們可以了解一下~
以上是Redis使用單線程為什麼還這麼快的詳細內容。更多資訊請關注PHP中文網其他相關文章!