面試官問,Redis 是單執行緒還是多執行緒?我懵了

我們平時看到介紹 Redis 的文章,都會說 Redis 是單執行緒的。但是我們學習的時候,比如 Redis 的 bgsave 命令,它的作用是在後台非同步保存當前資料庫的數據到磁碟,那既然是非同步了,肯定是由別的執行緒去完成的,這怎麼還能說 Redis 是單執行緒的呢?

其實通常說的 Redis 是單執行緒,主要是指 Redis 對外提供鍵值存儲服務的主要流程,即網路 IO 和鍵值對讀寫是由⼀個執行緒來完成的。除此外 Redis 的其他功能,比如持久化、 非同步刪除、集群數據同步等,是由額外的執行緒執⾏的。在這一點上 Node 也是一樣的,一般提到 Node 也是單執行緒的,但其實 Node 只有一個主執行緒是單執行緒,其他非同步任務則由其他執行緒完成。這樣做的原因是防止有同步程式碼阻塞,導致主執行緒被佔用後影響後續的程式程式碼執行。

因此,嚴格地說 Redis 並不是單執行緒。但是我們⼀般把 Redis 稱為單執行緒高性能,這樣顯得 Redis 更強一些。

Redis 為什麼用單執行緒

Redis 為什麼用單執行緒?在回答這個問題前,先來看大家都很熟悉的資料庫 MySQL,它使用的就是多執行緒。MySQL 不會每有一個連接就創建一個執行緒,因為執行緒過多會帶來額外的開銷,其中包括創建銷毀執行緒的開銷、調度執行緒的開銷等,同時也會降低電腦的整體性能。這個正是多執行緒會遇到的難點。

此外多執行緒系統中通常會存在被多執行緒同時訪問的共享資源,比如一個共享的數據結構,當有多個進程要修改這個共享資源時,為了保證共享資源的正確性,就需要有額外的機制進行保證,而這個額外的機制,也會帶來額外的開銷。還是以 MySQL 舉例,MySQL 引入了鎖機制來解決這個問題。

從上面不難看出,多執行緒開發中並發訪問控制是⼀個難點,需要精細的設計才能處理。如果只是簡單地處理,比如簡單地采⽤⼀個粗粒度互斥鎖,只會出現不理想的結果。即便增加了執行緒,系統吞吐率也不會隨著執行緒的增加而增加,因為大部分執行緒還在等待獲取訪問共享資源的互斥鎖。而且,大部分採用多執行緒開發引入的同步原語保護共享資源的並發訪問,也會降低系統程式碼的易調試性和可維護性。

而正是以上這些問題,才讓 Redis 采⽤了單執行緒模式。

看到這裡大家可能有點疑惑,前面說了 Redis 不是單執行緒,現在我們也說了 Redis 的鍵值對讀寫操作使用採用了單執行緒模式,那麼它的其他執行緒是是什麼樣的呢?

主進程的其它執行緒

Redis 3.0 版本後,主進程中除了主執行緒處理網路 IO 和命令操作外,還有 3 個輔助 BIO 執行緒。這 3 個 BIO 執行緒分別負責處理,文件關閉、AOF 緩衝數據刷新到磁碟,以及清理對象這三個任務隊列,從而避免這些任務對主 IO 執行緒的影響。

Redis 在啟動時,會同時啟動這三個 BIO 執行緒,但是 BIO 執行緒只有在需要執行相關類型後台任務時才會喚醒,其他時間會休眠等待任務。

多進程

除了主進程,在以下場景如果需要進行重負荷任務的處理,Redis 會 fork 一個子進程來處理:

  • 收到 bgrewriteaof 命令: Redis fork 一個子進程,然後子進程往臨時 AOF文件中寫入重建資料庫狀態的所有命令。寫入完畢後,子進程會通知父進程把新增的寫操作追加到臨時 AOF 文件。最後將臨時文件替換舊的 AOF 文件,並重命名。

  • 收到 bgsave 命令: Redis 構建子進程,子進程將記憶體中的所有數據通過快照做一次持久化落地,寫入到 RDB 中。

  • 當需要進行全量複製: master 啟動一個子進程,子進程將資料庫快照保存到 RDB 文件。在寫完 RDB 快照文件後,master 會把 RDB 發給 slave,同時將後續新的寫指令都同步給 slave。

Redis6.0 多執行緒

多執行緒是 Redis6.0 推出的一個新特性。正如上面所說 Redis 是核心執行緒負責網路 IO ,命令處理以及寫數據到緩衝,而隨著網路硬體的性能提升,單個主執行緒處理⽹絡請求的速度跟不上底層⽹絡硬體的速度,導致網路 IO 的處理成為了 Redis 的性能瓶頸。

而 Redis6.0 就是從單執行緒處理網路請求到多執行緒處理,通過多個 IO 執行緒並⾏處理網路操作提升實例的整體處理性能。需要注意的是對於讀寫命令,Redis 仍然使⽤單執行緒來處理,這是因為繼續使⽤單執行緒執行命令操作,就不⽤為了保證 Lua 腳本、事務的原⼦性,額外開發多執行緒互斥機制了。

需要注意的是在 Redis6.0 中,多執行緒機制默認是關閉的,需要在 redis.conf 中完成以下兩個設置才能啟用多執行緒。

  • 設置 io-thread-do-reads 配置項為 yes,表示啟用多執行緒。
io-threads-do-reads yes
  • 設置執行緒個數。⼀般來說,執行緒個數要小於 Redis 實例所在機器的 CPU 核數, 例如,對於⼀個 8 核的機器來說,Redis 官⽅建議配置 6 個 IO 執行緒。
io-threads 6

多執行緒流程

來具體看一下在 Redis6.0 中,主執行緒和 IO 執行緒是如何協作完成請求處理的。

整體流程示意圖

全部流程分為以下 4 階段:

階段一:服務端和客⼾端建立 Socket 連接,並分配處理執行緒

當有客⼾端請求和實例建立 Socket 連接時,主執行緒會創建和客戶端的連接,並把 Socket 放入全局等待隊列中。然後主執行緒通過輪詢方法把 Socket 連接分配給 IO 執行緒。

階段二:IO 執行緒讀取並解析請求

主執行緒把 Socket 分配給 IO 執行緒後,會進⼊阻塞狀態等待 IO 執行緒完成客戶端請求讀取和解析。

階段三:主執行緒執⾏請求操作

IO 執行緒解析完請求後,主執行緒以單執行緒的⽅式執⾏這些命令操作。

階段四:IO 執行緒回寫 Socket 和主執行緒清空全局隊

主執行緒執行完請求操作後,會把需要返回的結果寫入緩衝區。然後,主執行緒會阻塞等待 IO 執行緒把這些結果回寫到 Socket 中,並返回給客戶端。等到 IO 執行緒回寫 Socket 完畢,主執行緒會清空全局隊列,等待客戶端的後續請求。

總結

看完了這篇文章,相信大家對 Redis 是單執行緒的說法已經有了大致概念。我們說它是單執行緒,主要是因為在以前的版本中網路 IO 和鍵值對讀寫是由⼀個執行緒來完成的。而之所以說 Redis 是多執行緒,則是因為 Redis6.0 以後的版本里,網路 IO 的部分變為了多執行緒處理。而且除了主執行緒,還有 3 個輔助 BIO 執行緒,分別是 fsync 執行緒、close 執行緒、清理回收執行緒。當然不能忘記的是,想要體驗多執行緒機制,就得通過修改配置文件開啟多執行緒功能。

推薦閱讀

原創內容屢屢被盜?從源頭對資源盜用說NO

嚴重危害警告!Log4j 執行漏洞被公開!