Redis效能為何這麼強?

語言: CN / TW / HK

Redis的高效能怎麼做到的?

Redis這個NOSQL資料庫在計算機界可謂是無人不知,無人不曉。只要涉及到資料那麼就需要資料庫,資料庫型別很多,但是NOSQLkv記憶體資料庫也很多,redis作為其中一個是怎麼做到行業天花板的呢?是怎麼做到高效能的呢?怎麼做到高可用的呢?今天這篇八股文我就整理一些redis的設計寫寫,本篇還是偏關於高效能這一塊。

高效資料結構

Redis的資料庫相比傳統的關係資料庫,在資料結構上也是比較特殊的,它的所有資料型別都可以看做是一個map的結構,key作為查詢條件。

基本資料結構

Redis基於KV記憶體資料庫,它內部構建了一個雜湊表,根據指定的KEY訪問時,只需要O(1)的時間複雜度就可以找到對應的資料,而value的值又是一些擁有各種特性的資料結構,這就給redis在資料操作的時候提供很好的效能了。

基於記憶體儲存

相比傳統的關係資料庫,資料檔案可能以lsm tree 或者 b+ tree形式存在硬碟上,這個時候讀取檔案要有io操作了,而redis在記憶體中進行,並不會大量消耗CPU資源,所以速度極快。

儲存金字塔

記憶體從上圖可以看到它介於硬碟和cpu快取中間的,相比硬碟查詢資料肯定是快的,當然這裡筆者個人見解上,如果關係型資料庫把一些平凡操作的資料庫也放置在記憶體中快取,也會得到一些效能的提升,像作業系統裡面缺頁異常一樣處理,把資料片段通過一些特殊演算法快取在記憶體裡面,減少檔案io的開銷。

io多路複用

傳統對於併發情況,假如一個程序不行,那搞多個程序不就可以同時處理多個客戶端連線了麼?多程序是可以解決一些併發問題,但是還是有一些問題,上下文切換開銷,執行緒迴圈建立,從PCB來回恢復效率較低。隨著客戶端請求增多,那麼執行緒也隨著請求數量直線上升,如果是併發的時候涉及到資料共享訪問,有時候涉及到使用鎖來控制範圍順序,影響其他執行緒執行效率。(程序在Linux也可以理解為執行緒,每個程序只是有一個執行緒,當然這裡我上面寫的程序,別糾結這些。。。)

執行緒是執行在程序上下文的邏輯流,一個程序可以包含多個執行緒,多個執行緒執行在同一程序上下文中,因此可共享這個程序地址空間的所有內容,解決了程序與程序之間通訊難的問題,同時,由於一個執行緒的上下文要比一個程序的上下文小得多,所以執行緒的上下文切換,要比程序的上下文切換效率高得多。

redisNginx這種應用就是單執行緒的程式,為什麼他們能做到這麼強的效能?首先看一個例子:

  1. Blocking IO

中午吃飯,我給餐廳老闆說要一碗‘熱乾麵’,然後我就在那邊一直等著老闆做,老闆沒有做好,我就一直在哪裡等著什麼也不做,直到‘熱乾麵’做好。

這個流程就是我們常說的Blocking I/O如圖:

blocking io

同步阻塞 IO模型中,應用程式發起 read 呼叫後,會一直阻塞,直到核心把資料拷貝到使用者空間。

  1. Non Blocking IO

切換一下常見:

同樣你中午吃飯,給餐廳老闆說要一碗‘熱乾麵’,然後老闆開始做了,你每隔幾分鐘向老闆問一下‘好了嗎?’,直到老闆說好了,你取到‘熱乾麵’結束。

非阻塞io

同步非阻塞 IO模型中,應用程式會一直髮起read呼叫,等待資料從核心空間拷貝到使用者空間的這段時間裡,執行緒依然是阻塞的,直到在核心把資料拷貝到使用者空間,通過輪詢操作,避免了一直阻塞,取回熱乾麵的過程就是核心把準備好的資料交換到使用者空間過程。

綜上兩種模型,缺點都是差不多,都是在等待核心準備資料,然後阻塞等待,同樣逃不開阻塞這個問題,應用程式不斷進行I/O系統呼叫輪詢資料是否已經準備好的過程是十分消耗CPU資源的。

  1. I/O Multiplexing

還是之前那個例子:

中午吃飯,給餐廳老闆說要一碗‘熱乾麵’,然後老闆安排給下面的廚子做,具體哪個廚子做不知道,有好幾個廚子,然後老闆每隔一段時間詢問下面的廚子有木有做好,如果做好了,就通知我來去取餐。

多路複用

IO多路複用模型中,執行緒首先發起 select 呼叫,詢問核心資料是否準備就緒,等核心把資料準備好了,使用者執行緒再發起 read呼叫,read 呼叫的過程(資料從核心空間->使用者空間)還是阻塞的。

Reactor模式

Reactor通過 I/O複用程式監控客戶端請求事件,收到事件後通過任務分派器進行分發。針對建立連線請求事件,通過 Acceptor 處理,並建立對應的handler 負責後續業務處理,針對非連線事件,Reactor會呼叫對應的handler 完成 read->業務處理->write 處理流程,並將結果返回給客戶端,整個過程都在一個執行緒裡完成。

redis裡面模型

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

我們平時說的Redis單執行緒快是指它的請求處理過程非常地快!在單執行緒中監聽多個Socket的請求,在任意一個Socket可讀/可寫時,Redis去讀取客戶端請求,在記憶體中操作對應的資料,然後再寫回到Socket中。

單執行緒的好處:

  • 沒有了訪問共享資源加鎖的效能損耗
  • 開發和除錯非常友好,可維護性高
  • 沒有多個執行緒上下文切換帶來的額外開銷,不是沒有,是減少了

單執行緒不是沒有缺點,其實缺點也是很明顯的,如果前一個請求發生耗時比較久的操作,那麼整個Redis就會阻塞住,其他請求也無法進來,直到這個耗時久的操作處理完成並返回,其他請求才能被處理到,但是redis使用的Reactor 單執行緒模式來實現的可以緩解這種情況。

Redis 4.0之後的版本,引入多執行緒,而這個多執行緒是隻的非同步釋放記憶體,它主要是為了解決在釋放大記憶體資料導致整個redis阻塞的效能問題,單機redis如果處理大資料請求時還是會出現瓶頸,但是redis有叢集高可用解決方案可以解決,主節點只負責寫,從節點負責讀,io複用先寫到這裡,叢集高可用我會另外在出一篇文章。

寫時拷貝

有了高效的資料結構和io多路模型,目前能解決資料訪問效率問題,但是redis為了保證了資料不丟失有快照機制,說到快照那麼會操作磁碟,redis怎麼解決的在資料操作的時候並且還能保證資料記錄完整性的?不影響資料訪問效率的呢?

答案是用了寫時複製技術,什麼是寫時複製?如果你是一個科班的或者你的作業系統學的不錯的話,這個問題很清楚。

在作業系統設計中程序的記憶體可分為 虛擬記憶體實體記憶體,什麼是虛擬記憶體?你可以去看我上一篇文章Virtual Memoryredis會從主程序中通過fork()系統呼叫,建立一個子程序,將父程序的 虛擬記憶體實體記憶體 對映關係複製到子程序中,並將設定記憶體共享的,子程序只負責將記憶體裡面資料寫入到rdb進行持久化操作,如果在操作的時候主程序對記憶體修改了,使用寫時拷貝技術,將對應的記憶體建立一個副本然後進行寫入持久化。

示意圖

如上圖主程序則提供服務,只有當有人修改當前記憶體資料時,才去複製被修改的記憶體頁,用於生成快照。

管道通訊

除了本地伺服器記憶體和資料結構的操作影響客戶端讀寫效率的還有網路原因。redis的通訊協議是用一種檔案協議,有興趣自己去研究研究吧,我這裡不打算寫。每次客戶端操作的時候,命令和元資料都被打包成redis協議進行傳輸到伺服器上。

按照這樣那每個命令的執行時間:客戶端傳送時間 + 伺服器處理和返回時間 + 一個網路來回的時間。

資料包來回

從上圖可以看出來如果每操作一條命令,那麼就要執行一次網路io,如果客戶端頻繁操作資料那麼就頻繁網路操作,這個過程也是非常耗時的,影響效能的。redis在客戶端程式中做了一些優化引入了一個管道(pipelining)概念。

管道會把多條無關命令批量執行,以減少多個命令分別執行帶來的網路互動時間,在一些批量操作資料的場景。

小結

簡單始於複雜!,別看客戶端就幾個簡單api call的事情,這後面還有很多設計值得去學習,看完這篇八股文你或許對redis高效能有新的認識了,不要小看某些細節優化和解決方案選型,有時候可以帶來明顯效能提升。當然這篇文章沒有把redis設計寫完,例如還有aof的核心檔案描述符對映,非同步寫資料到硬碟上,零拷貝技術等等。。。。後續文章將會更新redis高可用是怎麼做到的?