Redis不是一直號稱單執行緒效率也很高嗎,為什麼又採用多執行緒了?

HollisChuang發表於2021-03-15

Redis是目前廣為人知的一個記憶體資料庫,在各個場景中都有著非常豐富的應用,前段時間Redis推出了6.0的版本,在新版本中採用了多執行緒模型。

因為我們公司使用的記憶體資料庫是自研的,按理說我對Redis的關注其實並不算多,但是因為Redis用的比較廣泛,所以我需要了解一下這樣方便我進行面試。

總不能候選人用過Redis,但是我非要問人家阿里的Tair是怎麼回事吧。

所以,在Redis 6.0 推出之後,我想去了解下為什麼採用多執行緒,現在採用的多執行緒和以前版本有什麼區別?為什麼這麼晚才使用多執行緒?

Redis不是已經採用了多路複用技術嗎?不是號稱很高的效能了嗎?為啥還要採用多執行緒模型呢?

本文就來分析下這些問題以及背後的思考。

Redis為什麼最開始被設計成單執行緒的?

Redis作為一個成熟的分散式快取框架,它由很多個模組組成,如網路請求模組、索引模組、儲存模組、高可用叢集支撐模組、資料操作模組等。

很多人說Redis是單執行緒的,就認為Redis中所有模組的操作都是單執行緒的,其實這是不對的。

我們所說的Redis單執行緒,指的是"其網路IO和鍵值對讀寫是由一個執行緒完成的",也就是說,Redis中只有網路請求模組和資料操作模組是單執行緒的。而其他的如持久化儲存模組、叢集支撐模組等是多執行緒的。

所以說,Redis中並不是沒有多執行緒模型的,早在Redis 4.0的時候就已經針對部分命令做了多執行緒化。

那麼,為什麼網路操作模組和資料儲存模組最初並沒有使用多執行緒呢?

這個問題的答案比較簡單!因為:"沒必要!"

為什麼沒必要呢?我們先來說一下,什麼情況下要使用多執行緒?

多執行緒適用場景

一個計算機程式在執行的過程中,主要需要進行兩種操作分別是讀寫操作和計算操作。

其中讀寫操作主要是涉及到的就是I/O操作,其中包括網路I/O和磁碟I/O。計算操作主要涉及到CPU。

而多執行緒的目的,就是通過併發的方式來提升I/O的利用率和CPU的利用率。

那麼,Redis需不需要通過多執行緒的方式來提升提升I/O的利用率和CPU的利用率呢?

首先,我們可以肯定的說,Redis不需要提升CPU利用率,因為Redis的操作基本都是基於記憶體的,CPU資源根本就不是Redis的效能瓶頸。

所以,通過多執行緒技術來提升Redis的CPU利用率這一點是完全沒必要的。

那麼,使用多執行緒技術來提升Redis的I/O利用率呢?是不是有必要呢?

Redis確實是一個I/O操作密集的框架,他的資料操作過程中,會有大量的網路I/O和磁碟I/O的發生。要想提升Redis的效能,是一定要提升Redis的I/O利用率的,這一點毋庸置疑。

但是,提升I/O利用率,並不是只有採用多執行緒技術這一條路可以走!

多執行緒的弊端

我們在很多文章中介紹過一些Java中的多執行緒技術,如記憶體模型、鎖、CAS等,這些都是Java中提供的一些在多執行緒情況下保證執行緒安全的技術。

執行緒安全:是程式設計中的術語,指某個函式、函式庫在併發環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變數,使程式功能正確完成。

和Java類似,所有支援多執行緒的程式語言或者框架,都不得不面對的一個問題,那就是如何解決多執行緒程式設計模式帶來的共享資源的併發控制問題。

雖然,採用多執行緒可以幫助我們提升CPU和I/O的利用率,但是多執行緒帶來的併發問題也給這些語言和框架帶來了更多的複雜性。而且,多執行緒模型中,多個執行緒的互相切換也會帶來一定的效能開銷。

所以,在提升I/O利用率這個方面上,Redis並沒有採用多執行緒技術,而是選擇了多路複用 I/O技術。

小結

Redis並沒有在網路請求模組和資料操作模組中使用多執行緒模型,主要是基於以下四個原因:

  • 1、Redis 操作基於記憶體,絕大多數操作的效能瓶頸不在 CPU
  • 2、使用單執行緒模型,可維護性更高,開發,除錯和維護的成本更低
  • 3、單執行緒模型,避免了執行緒間切換帶來的效能開銷
  • 4、在單執行緒中使用多路複用 I/O技術也能提升Redis的I/O利用率

還是要記住:Redis並不是完全單執行緒的,只是有關鍵的網路IO和鍵值對讀寫是由一個執行緒完成的。

Redis的多路複用

多路複用這個詞,相信很多人都不陌生。我之前的很多文章中也夠提到過這個詞。

其中在介紹Linux IO模型的時候我們提到過它、在介紹HTTP/2的原理的時候,我們也提到過他。

那麼,Redis的多路複用技術和我們之前介紹的又有什麼區別呢?

這裡先講講Linux多路複用技術,就是多個程式的IO可以註冊到同一個管道上,這個管道會統一和核心進行互動。當管道中的某一個請求需要的資料準備好之後,程式再把對應的資料拷貝到使用者空間中。

多看一遍上面這張圖和上面那句話,後面可能還會用得到。

也就是說,通過一個執行緒來處理多個IO流。

IO多路複用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是類似的,但具體細節各有不同。

其實,Redis的IO多路複用程式的所有功能都是通過包裝作業系統的IO多路複用函式庫來實現的。每個IO多路複用函式庫在Redis原始碼中都有對應的一個單獨的檔案。

在Redis 中,每當一個套接字準備好執行連線應答、寫入、讀取、關閉等操作時,就會產生一個檔案事件。因為一個伺服器通常會連線多個套接字,所以多個檔案事件有可能會併發地出現。

一旦有請求到達,就會交給 Redis 執行緒處理,這就實現了一個 Redis 執行緒處理多個 IO 流的效果。

所以,Redis選擇使用多路複用IO技術來提升I/O利用率。

而之所以Redis能夠有這麼高的效能,不僅僅和採用多路複用技術和單執行緒有關,此外還有以下幾個原因:

  • 1、完全基於記憶體,絕大部分請求是純粹的記憶體操作,非常快速。

  • 2、資料結構簡單,對資料操作也簡單,如雜湊表、跳錶都有很高的效能。

  • 3、採用單執行緒,避免了不必要的上下文切換和競爭條件,也不存在多程式或者多執行緒導致的切換而消耗 CPU

  • 4、使用多路I/O複用模型

為什麼Redis 6.0 引入多執行緒

2020年5月份,Redis正式推出了6.0版本,這個版本中有很多重要的新特性,其中多執行緒特性引起了廣泛關注。

但是,需要提醒大家的是,Redis 6.0中的多執行緒,也只是針對處理網路請求過程採用了多執行緒,而資料的讀寫命令,仍然是單執行緒處理的。

但是,不知道會不會有人有這樣的疑問:

Redis不是號稱單執行緒也有很高的效能麼?

不是說多路複用技術已經大大的提升了IO利用率了麼,為啥還需要多執行緒?

主要是因為我們對Redis有著更高的要求。

根據測算,Redis 將所有資料放在記憶體中,記憶體的響應時長大約為 100 納秒,對於小資料包,Redis 伺服器可以處理 80,000 到 100,000 QPS,這麼高的對於 80% 的公司來說,單執行緒的 Redis 已經足夠使用了。

但隨著越來越複雜的業務場景,有些公司動不動就上億的交易量,因此需要更大的 QPS。

為了提升QPS,很多公司的做法是部署Redis叢集,並且儘可能提升Redis機器數。但是這種做法的資源消耗是巨大的。

而經過分析,限制Redis的效能的主要瓶頸出現在網路IO的處理上,雖然之前採用了多路複用技術。但是我們前面也提到過,多路複用的IO模型本質上仍然是同步阻塞型IO模型

下面是多路複用IO中select函式的處理過程:

從上圖我們可以看到,在多路複用的IO模型中,在處理網路請求時,呼叫 select (其他函式同理)的過程是阻塞的,也就是說這個過程會阻塞執行緒,如果併發量很高,此處可能會成為瓶頸。

雖然現在很多伺服器都是多個CPU核的,但是對於Redis來說,因為使用了單執行緒,在一次資料操作的過程中,有大量的CPU時間片是耗費在了網路IO的同步處理上的,並沒有充分的發揮出多核的優勢。

如果能採用多執行緒,使得網路處理的請求併發進行,就可以大大的提升效能。多執行緒除了可以減少由於網路 I/O 等待造成的影響,還可以充分利用 CPU 的多核優勢。

所以,Redis 6.0採用多個IO執行緒來處理網路請求,網路請求的解析可以由其他執行緒完成,然後把解析後的請求交由主執行緒進行實際的記憶體讀寫。提升網路請求處理的並行度,進而提升整體效能。

但是,Redis 的多 IO 執行緒只是用來處理網路請求的,對於讀寫命令,Redis 仍然使用單執行緒來處理。

那麼,在引入多執行緒之後,如何解決併發帶來的執行緒安全問題呢?

這就是為什麼我們前面多次提到的"Redis 6.0的多執行緒只用來處理網路請求,而資料的讀寫還是單執行緒"的原因。

Redis 6.0 只有在網路請求的接收和解析,以及請求後的資料通過網路返回給時,使用了多執行緒。而資料讀寫操作還是由單執行緒來完成的,所以,這樣就不會出現併發問題了。

參考資料:

https://www.cnblogs.com/Zzbj/p/13531622.html
https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348
https://jishuin.proginn.com/p/763bfbd2a1c2
《極客時間:Redis核心技術與實戰》

相關文章