Redis底層資料結構——壓縮列表

紅橙呀發表於2020-12-16

壓縮列表是什麼

Redis 中的壓縮列表是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構。列表中每個節點可以儲存一個位元組陣列或者一個整數值。

它的存在主要是為了節約記憶體。

壓縮列表應用場景

壓縮列表在 Redis 中主要作為了 List 列表和 Hash 雜湊兩種資料結構的底層實現之一。

在 List 列表中,要是列表中的儲存元素數量少且每個元素是小的整數值或者長度較短的字串,那麼列表將使用壓縮列表結構作為底層實現。

在 Hash 雜湊中,要是雜湊中的儲存鍵值對數量少且每個鍵值對的鍵和值是小的整數值或者長度較短的字串,那麼雜湊將使用壓縮列表結構作為底層實現。

壓縮列表結構內容

一個大概壓縮列表的結構示意圖:

屬性 zlbytes

屬性 zlbytes 佔用4個位元組的長度,儲存了整個壓縮列表佔用的總位元組數。

在對壓縮列表重新進行記憶體分配或者計算 zlend 屬性值的時候會用到。

屬性 zltail

屬性 zltail 佔用了4個位元組的長度,儲存了壓縮列表表尾節點距離壓縮列表的開始地址有多少個具體位元組。

通過這個屬性值,程式不用遍歷整個壓縮列表就可以在時間複雜度為 O(1) 的情況下,直接確定表尾節點的位置。

屬性 zllen

屬性 zltail 佔用了2個位元組的長度,儲存了壓縮列表節點的數量。

需要注意的是,當 zllen 的值為小於UINT16_MAX (65535) 的時候,這個值就是真實的壓縮列表節點數量,大於的時候,節點的真實數量是需要遍歷整個壓縮列表才能計算得出。

屬性 zlend

屬性 zlend 佔用了1個位元組的長度,主要用於表示它是在壓縮列表的最後位置。

屬性 entry

屬性 entry 是作為壓縮列表的節點,下面我們具體介紹壓縮列表節點 entry。

壓縮列表節點結構內容

壓縮列表節點結構抽象程式碼:

typedef struct zlentry {    

    unsigned int prevrawlensize, prevrawlen;   
    
    unsigned int lensize, len;    
    
    unsigned int headersize;    
      
    unsigned char encoding;   
       
    unsigned char *p;
    
} zlentry;

這是一個抽象的壓縮列表節點結構,實際儲存並不是這樣子的結構,只是這樣設計到時候存取相關操作方便。

屬性 prevrawlen、prevrawlensize

屬性 prevrawlen 為前置節點的長度,而 prevrawlensize 是代表儲存 prevrawlen 這個屬性所需的位元組大小

屬性 len、lensize

屬性 len 為當前節點值的長度,而 lensize 是代表儲存 len 屬性所需的位元組大小。(注意:當節點儲存的是字串的時候,len 為字串長度,如果儲存的是整數值,len 為整數值的位元組長度)

屬性 headersize

屬性 headersize 為當前節點 header 的長度,等於 prevrawlensize + lensize

屬性 encoding

屬性 encoding 為當前節點值所使用的編碼型別

屬性 p

屬性 p 為一個指標,指向了當前節點的記憶體地址

壓縮列表節點具體儲存結構

一個壓縮列表節點具體的結構應該是這樣子的:
在這裡插入圖片描述

previous_entry_length

屬性 previous_entry_length 儲存了前置節點的長度。

  • 當前置節點的長度小於 254 位元組的時候,那麼 previous_entry_length 的長度將為1個位元組,前置節點的長度就儲存在這個位元組裡面。
  • 當前置節點的長度大於等於 254 位元組的時候,那麼 previous_entry_length 的長度將為5個位元組,這5個位元組的第一個位元組將被設定為254,然後剩下的四個位元組儲存前置節點的長度。

程式主要可以通過這個前置節點的長度,以及根據當前節點的起始地址來計算出前置節點的起始地址。壓縮列表的從表頭到表尾的遍歷操作就是通過這樣的原理來實現的。

encoding

屬性 encoding 記錄了對應節點的 content 屬性所儲存資料的型別以及長度。

encoding 的長度可能是一個位元組、兩個位元組或者五個位元組。它具體多少個位元組,取決於 encoding 值的最高兩位。當最高兩位為00、01、10開頭的時候,代表 content 儲存的是字串且 encoding 的長度分別為一個位元組、兩個位元組或者五個位元組。而為11開頭的時候,即 content 儲存的是整數且 encoding 的長度為一個位元組。

00開頭的時候:

encoding 佔用了一個位元組的空間,表示 content 能儲存長度小於等於 2^6-1 位元組的字串

01開頭的時候:

encoding 佔用了兩個位元組的空間,表示 content 能儲存長度小於等於 2^14-1 位元組的字串

10開頭的時候:

encoding 佔用了五個位元組的空間,第一位元組的後六位由0填充,剩下的表示 content 能儲存長度小於等於 2^32-1 位元組的字串

11開頭的時候:

encoding 佔用了一個位元組的空間,且由於編碼的不同,content 儲存的整數值範圍都是不同的

  • encoding 值: 11000000 ,content 能儲存 int16_t 型別的整數
  • encoding 值: 11010000 ,content 能儲存 int32_t 型別的整數
  • encoding 值: 11100000 ,content 能儲存 int64_t 型別的整數
  • encoding 值: 11110000 ,content 能儲存 24 位有符號整數
  • encoding 值: 11111110 ,content 能儲存 8 位有符號整數
  • encoding 值: 1111xxxx ,content 能儲存 0-12 之間的值,沒有明確的型別
content

屬性 content 儲存了壓縮列表節點的值,節點值可以是一個字串或者是整數值,值的型別和長度由 encoding 屬性值決定

壓縮列表的連鎖更新

假如在一個壓縮列表中,有多個連續節點 e1 至 eN 且他們各自的長度都在250位元組到253位元組之間,加上上面提到的 previous_entry_length 的相關內容,這個屬性記錄每個前置節點的長度的時候,值將都是一個位元組長度的。

在這裡插入圖片描述

此時我們把一個長度大於等於254位元組的新節點 new 設定到壓縮列表的表頭的時候,那麼 new 節點將成為 e1 節點的前置節點,而 e1 節點的 previous_entry_length 值只有1位元組的長度,沒有儲存表示前置節點長度的5個位元組長度,所以程式分配空間給 e1 多四個位元組儲存,接著 e2 也需要增加空間儲存 e1 的長度,然後以此類推,引發了後面所有的節點需要更新空間,這就是連鎖更新。

連鎖更新會導致在最壞的情況下要對壓縮列表 N 次空間重新分配,而每次空間重新分配的複雜度為 O(N),所以連鎖更新的最壞時間複雜度為 O(N^2)

不過這種是需要恰好多個連續節點都是在 250-253位元組之間,連鎖更新才可能發生,這種情況概率很低。又假如是幾個連續這樣的 250-253 位元組之間,對效能是不會造成影響的。

總結

先對 Redis 壓縮列表的含義、應用場景講述,瞭解了壓縮列表主要是為了節約記憶體的資料結構,再接著對其資料結構的詳細瞭解以及其主要的連鎖更新操作,深入瞭解了壓縮列表的底層。

參考:《 Redis設計與實現 》

更多Java後端開發相關技術,可以關注公眾號「 紅橙呀 」。

相關文章