Redis為什麼那麼快?

公眾號老韓隨筆發表於2021-07-19

資料庫有很多,為什麼Redis能有如此突出的表現呢?一方面,因為它是記憶體資料庫,所有操作都在記憶體上完成。另外一方面就要歸功於他的資料結構。高效的資料結構是Redis快速處理的基礎。今天我們就來聊聊了Redis的資料型別以及對應的資料結構。

首先Redis有5大基本型別:

1.String(字串)

2.List(列表)

3.Hash(雜湊)

4.Set(集合)

5.Zset(Sorted Set 有序集合)

他們的底層實現簡單來說一共有6種,分別是簡單的動態字串、雙向連結串列、壓縮列表、雜湊表、跳錶以及整數陣列。他們和資料型別的對應關係如下所示:

Redis為什麼那麼快?

可以看到這些資料結構都是值的底層實現,那鍵和值之間是用什麼資料結構來進行組織的呢?為了實現從鍵到值的快速訪問,Redis使用了一個雜湊表來儲存所有的鍵值對。

Redis為什麼那麼快?

雜湊表的最大好處很明顯,就是可以以O(1)的時間複雜度來快速查詢到鍵值對。但他有一個潛在的風險點,當你往Redis裡寫入大量的資料就會出現雜湊表的衝突問題以及rehash帶來的操作阻塞問題。

Redis解決雜湊衝突的方式,就是鏈式雜湊。鏈式雜湊很容易理解,就是指同一個hash桶中的多個元素用一個連結串列來儲存。如下圖所示:

Redis為什麼那麼快?

這就出現一個問題,雜湊衝突連結串列上的元素只能通過指標逐一查詢再操作。如果雜湊表裡寫入的資料越來越多,雜湊衝突也會越來越多,這就會導致某些雜湊衝突鏈過長,進而導致鏈上的元素查詢耗時長,效率低。這對求快的redis來說是不能接受的。

所以Redis會對雜湊表做rehash操作。rehash也就是增加現有的雜湊桶的數量,讓逐漸增多的entry元素能在更多的桶之間分散儲存,減少單個桶中的元素個數,從而減少衝突。

為了使rehash更高效,Redis預設使用2個全域性雜湊表:雜湊表1和雜湊表2。一開始,當你剛插入資料時,預設使用雜湊表1,此時雜湊表2並沒有分配空間。隨著資料的增多,Redis開始執行Rehash。主要分為以下3步:

  1. 給雜湊表2分配更大的空間。
  2. 把雜湊表1的資料重新對映並拷貝到雜湊表2。
  3. 釋放雜湊表1的空間。

到此我們可以從雜湊表1切換到雜湊表2,用容量更大的雜湊表2來儲存更多的資料,而原來的雜湊表1留做下一次rehash擴容備用。

可以看到第二步會涉及到大量的資料拷貝,如果一次性把雜湊表1全部都遷移完,會造成Redis執行緒阻塞,無法服務其他請求。為了避免這個問題,Redis採用了漸進式的Rehash。簡單來說就是在第二步拷貝資料時,仍然正常處理客戶端的請求,每處理一個請求,從雜湊表1的第一個索引位置開始,順帶著將這個索引位置上的所有entries拷貝到雜湊表2中;等處理下一個請求時,再順帶拷貝雜湊表1的下一個索引位置的entries。這樣就避免了一次性大量的資料拷貝,保證了資料的快速訪問。

目前為止,你已經瞭解了Redis的鍵和值是怎麼通過雜湊表來組織的了,對於String型別來說,找到雜湊桶就能直接增刪改查了,所以雜湊表O(1)的時間複雜度就是它的複雜度,但是對於集合型別來說,即使找到雜湊桶了,還需要在集合中進一步操作。接下來我們就分別聊聊集合型別的底層資料結構和操作複雜度。

我們在上面已經瞭解到集合型別的底層結構主要有5種:整數陣列、雙向連結串列、雜湊表、壓縮列表和跳錶。

其中,雜湊表的操作特點我們已經學過;整數陣列和雙向連結串列也很常見,主要是通過陣列下標和連結串列指標逐個訪問元素,操作複雜度是O(N),操作效率比較低。壓縮列表實際上類似於一個陣列,和陣列不同的是,壓縮列表在表頭有三個欄位zlbytes、zltail和zllen,分別表示列表的長度、列表尾的偏移量和列表中元素的個數;壓縮列表在表尾還有一個zlend表示列表結束。在壓縮列表中,如果我們要查詢定位第一個元素和最後一個元素,可以通過表頭直接定位,時間複雜度為O(1)。而查詢其它元素時,就沒有那麼高效了,只能逐個查詢,時間複雜度為O(N)。

圖片Redis為什麼那麼快?

下面我們來重點看一下跳錶。有序連結串列只能逐一查詢元素,導致操作起來非常緩慢,於是就出現了跳錶。跳錶是在連結串列的基礎上增加了多級索引,通過索引位置的幾個跳轉,實現資料的快速定位。如圖所示:

圖片Redis為什麼那麼快?

可以看到,這個查詢過程就是在多級索引上跳來跳去,最後定位到元素。當資料量很大時,跳錶的查詢複雜度是O(logN)。

圖片Redis為什麼那麼快?

好了,今天就分享到這裡,如果有什麼問題,可以在留言區留言。

Redis為什麼那麼快?

相關文章