面試時說Redis是單線程的,被噴慘了!

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis是單線程的,這話擱以前,是橫着走的,誰都知道的真理。現在不一樣,Redis 變了。再說這句話,多少得有質疑的語氣來跟你辯駁一番。意志不堅定的,可能就繳械投降,順着別人走了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到底是什麼樣的,各位看官請跟小萊一起往下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/19/1954db08df3b2338f91dd7a67c91eeae.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖注:思維導圖"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Reactor模式 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"反應器模式,你可能不太認識,如果看過上篇文章的話應該會有點印象。涉及到 Redis 線程它是一個繞不過去的話題。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1、傳統阻塞IO模型 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在講反應器模式前,這裏有必要提一下傳統阻塞IO模型的處理方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在傳統阻塞IO模型中,由一個獨立的 Acceptor 線程來監聽客戶端的連接,每當有客戶端請求過來時,它就會爲客戶端分配一個新的線程來進行處理。當同時有多個請求過來,服務端對應的就會分配相應數量的線程。這就會導致CPU頻繁切換,浪費資源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有的連接請求過來不做任何事情,但服務端還會分配對應的線程,這樣就會造成不必要的線程開銷。這就好比你去餐廳喫飯,你拿着菜單看了半天發現真他孃的貴,然後你就走人了。這段時間等你點菜的服務員就相當於一個對應的線程,你要點菜可以看作一個連接請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/90/90d3e39efa882ee3319e10bf00865fd8.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,每次建立連接後,當線程調用讀寫方法時,線程會被阻塞,直到有數據可讀可寫, 在此期間線程不能做其它事情。還是上邊餐廳喫飯的例子,你出去轉了一圈發現還是這家性價比最高。回到這家餐廳又拿着菜單看了半天,服務員也在旁邊等你點完菜爲止。這個過程中服務員什麼也不能做,只能這麼幹等着,這個過程相當於阻塞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/90/9035146a0795fc0feecc28523a2f9768.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你看這樣的方式,每來一個請求就要分配一個線程,並且還得阻塞地等線程處理完。有的請求還只是過來連接下,什麼操作也不幹,還得爲它分配一個線程,對服務器資源要求那得多高啊。遇到高併發場景,不敢想象。對於連接數目比較小的的固定架構倒是可以考慮。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2、僞異步IO模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能瞭解過一種通過線程池優化的解決方案,採用線程池和任務隊列的方式。這種被稱作僞異步IO模型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當有客戶端接入時,將客戶端的請求封裝成一個 task 投遞到後端線程池中來處理。線程池維護一個消息隊列和多個活躍線程,對消息隊列中的任務進行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f3/f359d0bbf4894b6f3eab2782b38ba6a8.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種解決方案,避免了爲每個請求創建一個線程導致的線程資源耗盡問題。但是底層仍然是同步阻塞模型。如果線程池內的所有線程都阻塞了,那麼對於更多請求就無法響應了。因此這種模式會限制最大連接數,並不能從根本上解決問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們繼續用上邊的餐廳來舉例,餐廳老闆在經營了一段時間後,顧客多了起來,原本店裏的5個服務員一對一服務的話根本對付不過來。於是老闆採用5個人線程池的方式。服務員服務完一個客人後立刻去服務另一個。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時問題出現了,有的客人點菜特別慢,服務員就得等待很長時間,直到客人點完爲止。如果5個客人都點的特別慢的話,這5個服務員就得一直等下去,就會導致其餘的顧客沒有人服務的狀態。這就是我們上邊所說的線程池所有線程都被阻塞的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼這種問題該如何解決呢?別急, Reactor 模式就要出場了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3、Reactor設計模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor 模式的基本設計思想是基於I/O複用模型來實現的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 這裏說下I/O複用模型。和傳統IO多線程阻塞不同,I/O複用模型中多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼意思呢?餐廳老闆也發現了顧客點餐慢的問題,於是他採用了一種大膽的方式,只留了一個服務員。當客人點餐的時候,這個服務員就去招待別的客人,客人點好餐後直接喊服務員來進行服務。這裏的顧客和服務員可以分別看作多個連接和一個線程。服務員阻塞在一個顧客那裏,當有別的顧客點好餐後,她就立刻去服務其他的顧客。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解了 reactor 的設計思想後,我們再來看下今天的主角單 reactor 單線程的實現方案:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/87/87939e8c41147fa9ff513fa1d3ad1703.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor 通過 I/O複用程序監控客戶端請求事件,收到事件後通過任務分派器進行分發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對建立連接請求事件,通過 Acceptor 處理,並建立對應的 handler 負責後續業務處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對非連接事件,Reactor 會調用對應的 handler 完成 read->業務處理->write 處理流程,並將結果返回給客戶端。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整個過程都在一個線程裏完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2d/2da8791bedddc80a447a31e3353c889a.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"單線程時代"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解了 Reactor 模式後,你可能會有一個疑問,這個和我們今天的主題有什麼關係呢。可能你不知道的是,Redis 是基於 Reactor 單線程模式來實現的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"IO多路複用程序接收到用戶的請求後,全部推送到一個隊列裏,交給文件分派器。對於後續的操作,和在 reactor 單線程實現方案裏看到的一樣,整個過程都在一個線程裏完成,因此 Redis 被稱爲是單線程的操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bf/bf49c50a8f175e074cf9cbed87c07677.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於單線程的 Redis 來說,基於內存,且命令操作時間複雜度低,因此讀寫速率是非常快的。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"多線程時代"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis6 版本中引入了多線程。上邊已經提到過 Redis 單線程處理有着很快的速度,那爲什麼還要引入多線程呢?單線程的瓶頸在什麼地方?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來看第二個問題,在 Redis 中,單線程的性能瓶頸主要在網絡IO操作上。也就是在讀寫網絡 read/write 系統調用執行期間會佔用大部分 CPU 時間。如果你要對一些大的鍵值對進行刪除操作的話,在短時間內是刪不完的,那麼對於單線程來說就會阻塞後邊的操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回想下上邊講得 Reactor 模式中單線程的處理方式。針對非連接事件,Reactor 會調用對應的 handler 完成 read->業務處理->write 處理流程,也就是說這一步會造成性能上的瓶頸。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis 在設計上採用將網絡數據讀寫和協議解析通過多線程的方式來處理,對於命令執行來說,仍然使用單線程操作。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Reactor模式"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳統阻塞IO模型客戶端與服務端線程1:1分配,不利於進行擴展。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"僞異步IO模型採用線程池方式,但是底層仍然使用同步阻塞方式,限制了最大連接數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor 通過 I/O複用程序監控客戶端請求事件,通過任務分派器進行分發。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"單線程時代"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於 Reactor 單線程模式實現,通過IO多路複用程序接收到用戶的請求後,全部推送到一個隊列裏,交給文件分派器進行處理。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"多線程時代"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單線程性能瓶頸主要在網絡IO上。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將網絡數據讀寫和協議解析通過多線程的方式來處理 ,對於命令執行來說,仍然使用單線程操作。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章