Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

石杉的架構筆記發表於2019-06-17

本文來自程式設計師歷小冰的投稿

在此感謝歷小冰同學的分享


Redis是一個開源的 key-value 儲存系統,它使用六種底層資料結構構建了包含字串物件、列表物件、雜湊物件、集合物件和有序集合物件的物件系統。

今天我們就通過12張圖來全面瞭解一下它的資料結構和物件系統的實現原理。

本文的內容如下:

  • 首先介紹六種基礎資料結構:動態字串,連結串列,字典,跳躍表,整數集合和壓縮列表。

  • 其次介紹 Redis 的物件系統中的字串物件(String)、列表物件(List)、雜湊物件(Hash)、集合物件(Set)和有序集合物件(ZSet)。

  • 最後介紹 Redis 的鍵空間和過期鍵( expire )實現。

資料結構

簡單動態字串

Redis 使用動態字串 SDS 來表示字串值。下圖展示了一個值為 Redis 的 SDS結構 :

  • len: 表示字串的真正長度(不包含NULL結束符在內)。

  • alloc: 表示字串的最大容量(不包含最後多餘的那個位元組)。

  • flags: 總是佔用一個位元組。其中的最低3個bit用來表示header的型別。

  • buf: 字元陣列。 Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

SDS 的結構可以減少修改字串時帶來的記憶體重分配的次數,這依賴於記憶體預分配和惰性空間釋放兩大機制。

當 SDS 需要被修改,並且要對 SDS 進行空間擴充套件時,Redis 不僅會為 SDS 分配修改所必須要的空間,還會為 SDS 分配額外的未使用的空間

  • 如果修改後, SDS 的長度(也就是len屬性的值)將小於 1MB ,那麼 Redis 預分配和 len 屬性相同大小的未使用空間。

  • 如果修改後, SDS 的長度將大於 1MB ,那麼 Redis 會分配 1MB 的未使用空間。

比如說,進行修改後 SDS 的 len 長度為20位元組,小於 1MB,那麼 Redis 會預先再分配 20 位元組的空間, SDS 的 buf陣列的實際長度(除去最後一位元組)變為 20 + 20 = 40 位元組。當 SDS的 len 長度大於 1MB時,則只會再多分配 1MB的空間。

類似的,當 SDS 縮短其儲存的字串長度時,並不會立即釋放多出來的位元組,而是等待之後使用。

連結串列

連結串列在 Redis 中的應用非常廣泛,比如列表物件的底層實現之一就是連結串列。除了連結串列物件外,釋出和訂閱、慢查詢、監視器等功能也用到了連結串列。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

Redis 的連結串列是雙向連結串列,示意圖如上圖所示。連結串列是最為常見的資料結構,這裡就不在細說。

Redis 的連結串列結構的dup 、 free 和 match 成員屬性是用於實現多型連結串列所需的型別特定函式:

  • dup 函式用於複製連結串列節點所儲存的值,用於深度拷貝。

  • free 函式用於釋放連結串列節點所儲存的值。

  • match 函式則用於對比連結串列節點所儲存的值和另一個輸入值是否相等。

字典

字典被廣泛用於實現 Redis 的各種功能,包括鍵空間和雜湊物件。其示意圖如下所示。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

Redis 使用 MurmurHash2 演算法來計算鍵的雜湊值,並且使用鏈地址法來解決鍵衝突,被分配到同一個索引的多個鍵值對會連線成一個單向連結串列。

跳躍表

Redis 使用跳躍表作為有序集合物件的底層實現之一。它以有序的方式在層次化的連結串列中儲存元素, 效率和平衡樹媲美 —— 查詢、刪除、新增等操作都可以在對數期望時間下完成, 並且比起平衡樹來說, 跳躍表的實現要簡單直觀得多。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

跳錶的示意圖如上圖所示,這裡只簡單說一下它的核心思想,並不進行詳細的解釋。

如示意圖所示,zskiplistNode 是跳躍表的節點,其 ele 是保持的元素值,score 是分值,節點按照其 score 值進行有序排列,而 level 陣列就是其所謂的層次化連結串列的體現。

每個 node 的 level 陣列大小都不同, level 陣列中的值是指向下一個 node 的指標和 跨度值 (span),跨度值是兩個節點的score的差值。越高層的 level 陣列值的跨度值就越大,底層的 level 陣列值的跨度值越小。

level 陣列就像是不同刻度的尺子。度量長度時,先用大刻度估計範圍,再不斷地用縮小刻度,進行精確逼近。

當在跳躍表中查詢一個元素值時,都先從第一個節點的最頂層的 level 開始。比如說,在上圖的跳錶中查詢 o2 元素時,先從o1 的節點開始,因為 zskiplist 的 header 指標指向它。

先從其 level[3] 開始查詢,發現其跨度是 2,o1 節點的 score 是1.0,所以加起來為 3.0,大於 o2 的 score 值2.0。所以,我們可以知道 o2 節點在 o1 和 o3 節點之間。這時,就改用小刻度的尺子了。就用level[1]的指標,順利找到 o2 節點。

整數集合

整數集合 intset 是集合物件的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時, Redis 就會使用整數集合作為集合物件的底層實現。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

如上圖所示,整數集合的 encoding 表示它的型別,有int16t,int32t 或者int64_t。其每個元素都是 contents 陣列的一個陣列項,各個項在陣列中按值的大小從小到大有序的排列,並且陣列中不包含任何重複項。length 屬性就是整數集合包含的元素數量。

壓縮列表

壓縮佇列 ziplist 是列表物件和雜湊物件的底層實現之一。當滿足一定條件時,列表物件和雜湊物件都會以壓縮佇列為底層實現。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

壓縮佇列是 Redis 為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構。它的屬性值有:

  • zlbytes : 長度為 4 位元組,記錄整個壓縮陣列的記憶體位元組數。

  • zltail : 長度為 4 位元組,記錄壓縮佇列表尾節點距離壓縮佇列的起始地址有多少位元組,通過該屬性可以直接確定尾節點的地址。

  • zllen : 長度為 2 位元組,包含的節點數。當屬性值小於 INT16_MAX時,該值就是節點總數,否則需要遍歷整個佇列才能確定總數。

  • zlend : 長度為 1 位元組,特殊值,用於標記壓縮佇列的末端。

中間每個節點 entry 由三部分組成:

  • previous_entry_length : 壓縮列表中前一個節點的長度,和當前的地址進行指標運算,計算出前一個節點的起始地址。

  • encoding: 節點儲存資料的型別和長度

  • content :節點值,可以為一個位元組陣列或者整數。

物件

上面介紹了 6 種底層資料結構,Redis 並沒有直接使用這些資料結構來實現鍵值資料庫,而是基於這些資料結構建立了一個物件系統.

這個系統包含字串物件、列表物件、雜湊物件、集合物件和有序集合這五種型別的物件,每個物件都使用到了至少一種前邊講的底層資料結構。

Redis 根據不同的使用場景和內容大小來判斷物件使用哪種資料結構,從而優化物件在不同場景下的使用效率和記憶體佔用。

Redis 的 redisObject 結構的定義如下所示。

typedef struct redisObject {    unsigned type:4;    unsigned encoding:4;    unsigned lru:LRU_BITS;     int refcount;    void *ptr;} robj;複製程式碼

其中 type 是物件型別,包括REDISSTRING, REDISLIST, REDISHASH, REDISSET 和 REDIS_ZSET。

encoding是指物件使用的資料結構,全集如下。Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

字串物件

我們首先來看字串物件的實現,如下圖所示。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

如果一個字串物件儲存的是一個字串值,並且長度大於32位元組,那麼該字串物件將使用 SDS 進行儲存,並將物件的編碼設定為 raw,如圖的上半部分所示。

如果字串的長度小於32位元組,那麼字串物件將使用embstr 編碼方式來儲存。

embstr 編碼是專門用於儲存短字串的一種優化編碼方式,這個編碼的組成和 raw 編碼一致,都使用 redisObject 結構和 sdshdr 結構來儲存字串,如上圖的下半部所示。

但是 raw 編碼會呼叫兩次記憶體分配來分別建立上述兩個結構,而 embstr 則通過一次記憶體分配來分配一塊連續的空間,空間中一次包含兩個結構。

embstr 只需一次記憶體分配,而且在同一塊連續的記憶體中,更好的利用快取帶來的優勢,但是 embstr 是隻讀的,不能進行修改,當一個 embstr 編碼的字串物件進行 append 操作時, redis 會現將其轉變為 raw 編碼再進行操作。

列表物件

列表物件的編碼可以是 ziplist 或 linkedlist。其示意圖如下所示。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

當列表物件可以同時滿足以下兩個條件時,列表物件使用 ziplist 編碼:

  • 列表物件儲存的所有字串元素的長度都小於 64 位元組。

  • 列表物件儲存的元素數量數量小於 512 個。

不能滿足這兩個條件的列表物件需要使用 linkedlist 編碼或者轉換為 linkedlist 編碼。

雜湊物件

雜湊物件的編碼可以使用 ziplist 或 dict。其示意圖如下所示。

當雜湊物件使用壓縮佇列作為底層實現時,程式將鍵值對緊挨著插入到壓縮佇列中,儲存鍵的節點在前,儲存值的節點在後。如下圖的上半部分所示,該雜湊有兩個鍵值對,分別是 name:Tom 和 age:25。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

當雜湊物件可以同時滿足以下兩個條件時,雜湊物件使用 ziplist 編碼:

  • 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於64位元組。

  • 雜湊物件儲存的鍵值對數量小於512個。

不能滿足這兩個條件的雜湊物件需要使用 dict 編碼或者轉換為 dict 編碼。

集合物件

集合物件的編碼可以使用 intset 或者 dict。

intset 編碼的集合物件使用整數集合最為底層實現,所有元素都被儲存在整數集合裡邊。

而使用 dict 進行編碼時,字典的每一個鍵都是一個字串物件,每個字串物件就是一個集合元素,而字典的值全部都被設定為NULL。如下圖所示。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

當集合物件可以同時滿足以下兩個條件時,物件使用 intset 編碼:

  • 集合物件儲存的所有元素都是整數值。

  • 集合物件儲存的元素數量不超過512個。

否則使用 dict 進行編碼。

有序集合物件

有序集合的編碼可以為 ziplist 或者 skiplist。

有序集合使用 ziplist 編碼時,每個集合元素使用兩個緊挨在一起的壓縮列表節點表示,前一個節點是元素的值,第二個節點是元素的分值,也就是排序比較的數值。

壓縮列表內的集合元素按照分值從小到大進行排序,如下圖上半部分所示。

有序集合使用 skiplist 編碼時使用 zset 結構作為底層實現,一個 zet 結構同時包含一個字典和一個跳躍表。

其中,跳躍表按照分值從小到大儲存所有元素,每個跳躍表節點儲存一個元素,其score值是元素的分值。而字典則建立一個一個從成員到分值的對映,字典的鍵是集合成員的值,字典的值是集合成員的分值。通過字典可以在O(1)複雜度查詢給定成員的分值。如下圖所示。

跳躍表和字典中的集合元素值物件都是共享的,所以不會額外消耗記憶體。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

當有序集合物件可以同時滿足以下兩個條件時,物件使用 ziplist 編碼:

  • 有序集合儲存的元素數量少於128個;

  • 有序集合儲存的所有元素的長度都小於64位元組。

否則使用 skiplist 編碼。

資料庫鍵空間

Redis 伺服器都有多個 Redis 資料庫,每個Redis 資料都有自己獨立的鍵值空間。每個 Redis 資料庫使用 dict 儲存資料庫中所有的鍵值對。

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!

鍵空間的鍵也就是資料庫的鍵,每個鍵都是一個字串物件,而值物件可能為字串物件、列表物件、雜湊表物件、集合物件和有序集合物件中的一種物件。

除了鍵空間,Redis 也使用 dict 結構來儲存鍵的過期時間,其鍵是鍵空間中的鍵值,而值是過期時間,如上圖所示。

通過過期字典,Redis 可以直接判斷一個鍵是否過期,首先檢視該鍵是否存在於過期字典,如果存在,則比較該鍵的過期時間和當前伺服器時間戳,如果大於,則該鍵過期,否則未過期。

END


個人公眾號:石杉的架構筆記(ID:shishan100)

歡迎長按下圖關注公眾號:石杉的架構筆記!

公眾號後臺回覆資料,獲取作者獨家祕製學習資料

石杉的架構筆記,BAT架構經驗傾囊相授

Redis 資料結構和物件系統,記住這 12 張圖就夠啦!



相關文章