面試時說Redis是單執行緒的,被噴慘了!

語言: CN / TW / HK

Redis是單執行緒的,這話擱以前,是橫著走的,誰都知道的真理。現在不一樣,Redis 變了。再說這句話,多少得有質疑的語氣來跟你辯駁一番。意志不堅定的,可能就繳械投降,順著別人走了。

到底是什麼樣的,各位看官請跟小萊一起往下:

圖注:思維導圖

Reactor模式

反應器模式,你可能不太認識,如果看過上篇文章的話應該會有點印象。涉及到 Redis 執行緒它是一個繞不過去的話題。

1、傳統阻塞IO模型

在講反應器模式前,這裡有必要提一下傳統阻塞IO模型的處理方式。

在傳統阻塞IO模型中,由一個獨立的 Acceptor 執行緒來監聽客戶端的連線,每當有客戶端請求過來時,它就會為客戶端分配一個新的執行緒來進行處理。當同時有多個請求過來,服務端對應的就會分配相應數量的執行緒。這就會導致CPU頻繁切換,浪費資源。

有的連線請求過來不做任何事情,但服務端還會分配對應的執行緒,這樣就會造成不必要的執行緒開銷。這就好比你去餐廳吃飯,你拿著選單看了半天發現真他孃的貴,然後你就走人了。這段時間等你點菜的服務員就相當於一個對應的執行緒,你要點菜可以看作一個連線請求。

同時,每次建立連線後,當執行緒呼叫讀寫方法時,執行緒會被阻塞,直到有資料可讀可寫, 在此期間執行緒不能做其它事情。還是上邊餐廳吃飯的例子,你出去轉了一圈發現還是這家價效比最高。回到這家餐廳又拿著選單看了半天,服務員也在旁邊等你點完菜為止。這個過程中服務員什麼也不能做,只能這麼幹等著,這個過程相當於阻塞。

你看這樣的方式,每來一個請求就要分配一個執行緒,並且還得阻塞地等執行緒處理完。有的請求還只是過來連線下,什麼操作也不幹,還得為它分配一個執行緒,對伺服器資源要求那得多高啊。遇到高併發場景,不敢想象。對於連線數目比較小的的固定架構倒是可以考慮。

2、偽非同步IO模型

你可能瞭解過一種通過執行緒池優化的解決方案,採用執行緒池和任務佇列的方式。這種被稱作偽非同步IO模型。

當有客戶端接入時,將客戶端的請求封裝成一個 task 投遞到後端執行緒池中來處理。執行緒池維護一個訊息佇列和多個活躍執行緒,對訊息佇列中的任務進行處理。

這種解決方案,避免了為每個請求建立一個執行緒導致的執行緒資源耗盡問題。但是底層仍然是同步阻塞模型。如果執行緒池內的所有執行緒都阻塞了,那麼對於更多請求就無法響應了。因此這種模式會限制最大連線數,並不能從根本上解決問題。

我們繼續用上邊的餐廳來舉例,餐廳老闆在經營了一段時間後,顧客多了起來,原本店裡的5個服務員一對一服務的話根本對付不過來。於是老闆採用5個人執行緒池的方式。服務員服務完一個客人後立刻去服務另一個。

這時問題出現了,有的客人點菜特別慢,服務員就得等待很長時間,直到客人點完為止。如果5個客人都點的特別慢的話,這5個服務員就得一直等下去,就會導致其餘的顧客沒有人服務的狀態。這就是我們上邊所說的執行緒池所有執行緒都被阻塞的情況。

那麼這種問題該如何解決呢?別急, Reactor 模式就要出場了。

3、Reactor設計模式

Reactor 模式的基本設計思想是基於I/O複用模型來實現的。

這裡說下I/O複用模型。和傳統IO多執行緒阻塞不同,I/O複用模型中多個連線共用一個阻塞物件,應用程式只需要在一個阻塞物件等待。當某個連線有新的資料可以處理時,作業系統通知應用程式,執行緒從阻塞狀態返回,開始進行業務處理。

什麼意思呢?餐廳老闆也發現了顧客點餐慢的問題,於是他採用了一種大膽的方式,只留了一個服務員。當客人點餐的時候,這個服務員就去招待別的客人,客人點好餐後直接喊服務員來進行服務。這裡的顧客和服務員可以分別看作多個連線和一個執行緒。服務員阻塞在一個顧客那裡,當有別的顧客點好餐後,她就立刻去服務其他的顧客。

瞭解了 reactor 的設計思想後,我們再來看下今天的主角單 reactor 單執行緒的實現方案:

Reactor 通過 I/O複用程式監控客戶端請求事件,收到事件後通過任務分派器進行分發。

針對建立連線請求事件,通過 Acceptor 處理,並建立對應的 handler 負責後續業務處理。

針對非連線事件,Reactor 會呼叫對應的 handler 完成 read->業務處理->write 處理流程,並將結果返回給客戶端。

整個過程都在一個執行緒裡完成。

單執行緒時代

瞭解了 Reactor 模式後,你可能會有一個疑問,這個和我們今天的主題有什麼關係呢。可能你不知道的是,Redis 是基於 Reactor 單執行緒模式來實現的。

IO多路複用程式接收到使用者的請求後,全部推送到一個佇列裡,交給檔案分派器。對於後續的操作,和在 reactor 單執行緒實現方案裡看到的一樣,整個過程都在一個執行緒裡完成,因此 Redis 被稱為是單執行緒的操作。

對於單執行緒的 Redis 來說,基於記憶體,且命令操作時間複雜度低,因此讀寫速率是非常快的。

多執行緒時代

Redis6 版本中引入了多執行緒。上邊已經提到過 Redis 單執行緒處理有著很快的速度,那為什麼還要引入多執行緒呢?單執行緒的瓶頸在什麼地方?

我們先來看第二個問題,在 Redis 中,單執行緒的效能瓶頸主要在網路IO操作上。也就是在讀寫網路 read/write 系統呼叫執行期間會佔用大部分 CPU 時間。如果你要對一些大的鍵值對進行刪除操作的話,在短時間內是刪不完的,那麼對於單執行緒來說就會阻塞後邊的操作。

回想下上邊講得 Reactor 模式中單執行緒的處理方式。針對非連線事件,Reactor 會呼叫對應的 handler 完成 read->業務處理->write 處理流程,也就是說這一步會造成效能上的瓶頸。

Redis 在設計上採用將網路資料讀寫和協議解析通過多執行緒的方式來處理,對於命令執行來說,仍然使用單執行緒操作。

總結

Reactor模式

  • 傳統阻塞IO模型客戶端與服務端執行緒1:1分配,不利於進行擴充套件。

  • 偽非同步IO模型採用執行緒池方式,但是底層仍然使用同步阻塞方式,限制了最大連線數。

  • Reactor 通過 I/O複用程式監控客戶端請求事件,通過任務分派器進行分發。

單執行緒時代

  • 基於 Reactor 單執行緒模式實現,通過IO多路複用程式接收到使用者的請求後,全部推送到一個佇列裡,交給檔案分派器進行處理。

多執行緒時代

  • 單執行緒效能瓶頸主要在網路IO上。

  • 將網路資料讀寫和協議解析通過多執行緒的方式來處理 ,對於命令執行來說,仍然使用單執行緒操作。

關於作者

作者:大家好,我是萊烏,BAT搬磚工一枚。從小公司進入大廠,一路走來收穫良多,想將這些經驗分享給有需要的人,因此建立了公眾號「IT界農民工」。定時更新,希望能幫助到你。