面試官問,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 執行漏洞被公開!