Redis 為什麼這麼快?

2023-08-29 06:01:35

前言

  作為一名後端軟體工程師,工作中你肯定和 Redis 打過交道。但是Redis 為什麼快呢?很多人只能答出Redis 因為它是基於記憶體實現的,但是對於其它原因都是模稜兩可。

那麼今天就一起來看看是Redis 為什麼快吧:

 

              Redis 為什麼這麼快?

 

一、基於記憶體實現

  Redis 是基於記憶體的資料庫,那不可避免的就要與磁碟資料庫做對比。對於磁碟資料庫來說,是需要將資料讀取到記憶體裡的,這個過程會受到磁碟 I/O 的限制。而對於記憶體資料庫來說,本身資料就存在於記憶體裡,也就沒有了這方面的開銷。

通過下面的表格我們可以知道讀取記憶體和讀取磁碟的效能差距。

計算機裝置

讀取的速度

類比

機械硬碟

0.1G/S

以機械盤為基準

固態盤

1.3G/S

13倍機械硬碟

記憶體

30G/S

300倍機械硬碟

L3

190G/S

1900倍機械硬碟

L2

200G/S

2000倍 機械硬碟

L1

800G/S

8000倍機械硬碟

 

二、高效儲存結構

  為了實現從鍵到值的快速存取,Redis 使用了一個雜湊表來儲存所有鍵值對。一個雜湊表,其實就是一個陣列,陣列的每個元素稱為一個雜湊桶。所以,我們常說,一個雜湊表是由多個雜湊桶組成的,每個雜湊桶中儲存了鍵值對資料。

                  全域性雜湊表

 

  雜湊桶中的 entry 元素中儲存了key和value指標,分別指向了實際的鍵和值,因為其value的多樣性,雜湊表中儲存的並不是具體的值,而是一個記憶體參照地址,在通過記憶體參照的地址查詢到對應的具體的值。這樣一來,即使value是一個集合,也可以通過*value指標被查詢到。因為這個雜湊表儲存了所有的鍵值對,所以,我也把它稱為全域性雜湊表。

  雜湊表的最大好處很明顯,就是讓我們可以用 O(1) 的時間複雜度來快速查詢到鍵值對:我們只需要計算鍵的雜湊值,就可以知道它所對應的雜湊桶位置,然後就可以存取相應的 entry 元素。但當你往 Redis 中寫入大量資料後,就可能發現操作有時候會突然變慢了。這其實是因為你忽略了一個潛在的風險點,那就是雜湊表的衝突問題和 rehash 可能帶來的操作阻塞。

  當你往雜湊表中寫入更多資料時,雜湊衝突是不可避免的問題。這裡的雜湊衝突,兩個 key 的雜湊值和雜湊桶計算對應關係時,正好落在了同一個雜湊桶中。

                  雜湊表的雜湊衝突

 

  Redis 解決雜湊衝突的方式,就是鏈式雜湊。鏈式雜湊也很容易理解,就是指同一個雜湊桶中的多個元素用一個連結串列來儲存,它們之間依次用指標連線。

 

三、單執行緒避免了上下文前切換

  省去了很多上下文切換的時間以及CPU消耗,不存在競爭條件,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,也不會出現死鎖而導致的效能消耗。

 

四、使用基於IO多路複用機制的執行緒模型,可以處理並行的連結。

  Redis採用了epoll 模型進行IO多路複用。Java中也有類似的模型比如NIO,才epoll模型之前還有selector、poll這裡不多講解,epoll模型可以參考下圖:

                  epoll 模型

 

五、漸進式ReHash

  Redis是當然如果這個陣列一直不變,那麼hash衝突會變很多,這個時候檢索效率會大打折扣,所以Redis就需要把陣列進行擴容(一般是擴大到原來的兩倍),但是問題來了,擴容後每個hash桶的資料會分散到不同的位置,這裡設計到元素的移動,必定會阻塞IO,所以這個ReHash過程會導致很多請求阻塞。

  為了避免這個問題,Redis 採用了漸進式 rehash。

  首先、Redis 預設使用了兩個全域性雜湊表:雜湊表 1 和雜湊表 2。一開始,當你剛插入資料時,預設使用雜湊表 1,此時的雜湊表 2 並沒有被分配空間。隨著資料逐步增多,Redis 開始執行 rehash。

  1、給雜湊表 2 分配更大的空間,例如是當前雜湊表 1 大小的兩倍

  2、把雜湊表 1 中的資料重新對映並拷貝到雜湊表 2 中

  3、釋放雜湊表 1 的空間

   在上面的第二步涉及大量的資料拷貝,如果一次性把雜湊表 1 中的資料都遷移完,會造成 Redis 執行緒阻塞,無法服務其他請求。此時,Redis 就無法快速存取資料了。

              漸進式rehash

 

  在Redis 開始執行 rehash,Redis仍然正常處理使用者端請求,但是要加入一個額外的處理:

  處理第1個請求時,把雜湊表 1中的第1個索引位置上的所有 entries 拷貝到雜湊表 2 中

  處理第2個請求時,把雜湊表 1中的第2個索引位置上的所有 entries 拷貝到雜湊表 2 中

  如此迴圈,直到把所有的索引位置的資料都拷貝到雜湊表 2 中。

  這樣就巧妙地把一次性大量拷貝的開銷,分攤到了多次處理請求的過程中,避免了耗時操作,保證了資料的快速存取。

  所以這裡基本上也可以確保根據key找value的操作在O(1)左右。

  過這裡要注意,如果Redis中有海量的key值的話,這個Rehash過程會很長很長,雖然採用漸進式Rehash,但在Rehash的過程中還是會導致請求有不小的卡頓。並且像一些統計命令也會非常卡頓:比如keys

按照Redis的設定每個範例能儲存的最大的key的數量為2的32次方,即2.5億,但是儘量把key的數量控制在千萬以下,這樣就可以避免Rehash導致的卡頓問題,如果數量確實比較多,建議採用分割區hash儲存。

 

六、快取時間戳

  我們平常使用系統時間戳時, 常常是不假思索地使用System.currentTimeMillis()或者new Date() .getTime() 來獲取系統的毫秒時間戳。但是Redis不能這樣做,因為每一次獲取系統時間戳都是一次系統呼叫,而且每次去系統呼叫是比較費時間的,作為單執行緒的Redis是無法承受的,所以它需要對於時間戳進行一次快取,由一個定時任務進行每毫秒更新時間戳,從而獲取時間戳都是直接從快取就取出。