《Redis設計與實現》筆記 -- 資料結構與物件

Rabbit_Judy發表於2019-04-01

1、簡單動態字串

Redis 沒有直接使用 C 語言傳統的字串表示,而是自己構建了一種簡單動態字串 (SDS),使用 SDS 作為 Redis 的預設字串表示。

1.1 SDS 定義

struct sdshdr {
    
// 記錄 buf 陣列中已經使用位元組的數量,等於 SDS 所儲存字串的長度
    int len;
    
    // 記錄 buf 陣列中未使用位元組的數量
    int free;
    
    // 位元組陣列,用於儲存字串
    char buf[];
};
複製程式碼

1.2 SDS 與 C 字串的區別

C字串 SDS
獲取字串長度的複雜度為O(N) 獲取字串長度的複雜度為O(1)
API不安全,可能造成緩衝區溢位 API安全,不會造成緩衝區溢位
修改字串長度N次必然需要執行N次記憶體重分配 修改字串長度N次最多需要執行N次記憶體重分配
只能儲存文字資料 可以儲存文字或者二進位制資料
可以使用所有<string.h>庫中的函式 可以使用部分<string.h>庫中的函式

2、連結串列

連結串列在 Redis 中的應用非常廣泛,比如列表鍵的底層實現之一就是連結串列。當一個列表鍵包含了數量比較多的元素,又或者列表中包含的元素都是比較長的字串時,Redis 就會使用連結串列作為列表鍵的底層實現。

2.1 Redis 連結串列結構

typedef struct list {
    // 表頭節點
    listNode *head;
    
    // 表尾節點
    listNode *tail;
    
    // 連結串列所包含的節點數量
    unsigned long len;
    
    // 節點值複製函式
    void *(*dup)(void *ptr);
    
    // 節點值釋放函式
    void (*free)(void *ptr);
    
    // 節點值對比函式
    int (*match)(void *ptr, void *key);
} list;
複製程式碼

由一個 list 結構和三個 listNode 結構組成的連結串列如下圖所示: ​​

在這裡插入圖片描述

2.2 Redis 連結串列實現特性

  • 雙端:連結串列節點帶有 prev 和 next 指標獲取某個節點的前置節點和後置節點的複雜度都是 O(1)。
  • 無環:表頭節點的 prev 指標和表尾節點的 next 指標都指向 NULL 對連結串列的訪問以 NULL 為終點。
  • 帶表頭指標和表尾指標:通過 list 結構的 head 指標和 tail 指標,程式獲取連結串列的表頭節點和表尾節點的複雜度為 O(1)。
  • 帶連結串列長度計數器:通過 list 結構的 len 屬性來對 list 持有的連結串列節點進行計數,程式獲取連結串列中節點數量的複雜度為 O(1)。
  • 多型:連結串列節點使用 void* 指標來儲存節點值,並且可以通過 list 結構的 dup、free、match 三個屬性節點值設定型別特定函式,所以連結串列可以用於儲存各種不同型別的值。

3、字典

字典,是一種用於儲存鍵值對 (key-valuepair) 的抽象資料結構,在字典中一個鍵 (key) 可以和一個值 (value) 進行關聯,這些關聯的鍵和值就稱為鍵值對。

Redis 的資料庫就是使用字典作為底層實現,對資料庫的增、刪、改、查操作也是構建在對字典的操作之上的。

字典還是雜湊鍵的底層實現之一,當一個雜湊鍵包含的鍵值對比較多時,又或者鍵值對中的元素都是比較長的字串時,Redis 就會使用字典作為雜湊鍵的底層實現。

3.1 字典實現

Redis 的字典使用雜湊表作為底層實現,一個雜湊表裡面可以有多個雜湊表節點,而每個雜湊表節點就儲存了字典中的一個鍵值對。

Redis 中的字典結構為:

typedef struct dict {
    // 型別特定函式
    dictType *type;
    
    // 私有資料
    void *privdata;
    
    // 雜湊表
    dictht ht[2];
    
// rehash 索引
//rehash 不在進行時,值為 -1
    int trehashidx;
}dict;
複製程式碼

ht 屬性是一個包含兩個項的陣列,陣列中的每個項都是一個 dictht 雜湊表,一般情況下,字典只使用 ht[0] 雜湊表,ht[1] 雜湊表只會在對 ht[0 雜湊表進行 rehash 時使用。

下圖展示了一個普通狀態下(沒有進行 rehash)的字典:

在這裡插入圖片描述

3.2 雜湊

當字典被用作資料庫的底層實現,或者雜湊鍵的底層實現,Redis 使用 MurmurHash2 演算法來計算鍵的雜湊值。

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

在對雜湊表進行擴充套件或者收縮操作時,程式需要將現有的雜湊表包含的所有鍵值對rehash到新的雜湊表裡面,並且這個 rehash 過程並不是一次性地完成的,而是漸進式地完成的。

4、跳躍表

跳躍表是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。

跳躍表支援平均 O(logN),最壞 O(N) 的複雜度的節點查詢,還可以通過順序性操作來批量處理節點。

Redis 使用跳躍表作為有序集合鍵的底層實現之一。

跳躍表由 zskiplistNode 和 zskiplist 兩個結構定義,其中 zskiplistNode 結構用於表示跳躍表節點,zskiplist 結構用於儲存跳躍表節點的相關資訊,跳躍表示例如下圖所示:

在這裡插入圖片描述

zskiplist

header:指向跳躍表的表頭節點。

tail:指向跳躍表的表尾節點。

level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層不計算在內)。

length:記錄跳躍表的長度(表頭節點的層不計算在內)。

zskiplistNode

level:層,每個層都有前進指標和跨度兩個屬性。

backward:後退指標,節點中用BW字樣進行標記,在程式從表尾向表頭遍歷時使用。

score:分值,跳躍表中節點按各自儲存的分值從小到大排列。

obj:成員物件。

5、整數集合

整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現。它可以儲存型別為int16_t、int32_t、int64_t的整數值,並且保證集合中不會出現重複元素。

5.1 整數集合的結構

typedef struct intset {
    // 編碼方式
    uint32_t encoding;
    
    // 集合包含的元素數量
    unit32_t length;
    
    // 儲存元素的陣列
    int8_t contents[];
}inset;
複製程式碼

下圖所示為一個包含五個int16_t型別整數值的整數集合:

在這裡插入圖片描述

5.2 升級

當我們要將一個新的元素新增到整數集合裡面,並且新元素的型別比整數集合現有的所有元素的型別都要長,整數集合需要先進行升級(upgrade),然後才能將新元素新增到整數集合裡面。

步驟:

  • 根據新元素的型別,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間。
  • 將底層陣列現在的所有元素都轉換成與新元素相同的型別,並將型別轉換後的元素放置到正確的位置上,並且在放置元素過程中,需要維持底層陣列的有序性質不變。
  • 將新元素新增到底層陣列裡面。

5.3 總結

  • 整數集合的底層實現為陣列,這個陣列以有序、無重複的方式儲存集合元素,在有需要時,程式會根據新新增元素的型別,改變這個陣列的型別。
  • 升級操作為整數集合帶來了操作上的靈活性,並儘可能的節約了記憶體。
  • 整數集合只支援升級操作,不支援降級操作。

6、壓縮列表

壓縮列表 (ziplist) 是一種為節約記憶體而開發的順序型資料結構,是列表鍵和雜湊鍵的底層實現之一。它可以包含多個節點,每個節點可以儲存一個位元組陣列或者整數值。新增新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作,但這種操作出現的機率並不高。

當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字串,那麼 Redis 就會使用壓縮列表來做列表建的底層實現。

當一個雜湊鍵只包含少量鍵值對,並且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度比較短的字串,那麼 Redis 就會使用壓縮列表來做雜湊鍵的底層實現。

下圖所示為包含三個節點的壓縮列表:

在這裡插入圖片描述

  • zlbytes:記錄整個壓縮列表佔用的記憶體位元組數;
  • zltail:記錄壓縮列表表尾節點距離起始地址有多少子節;
  • zllen:記錄壓縮列表包含的節點數量;
  • entryx:壓縮列表包含的各個節點,節點長度由儲存的內容決定;
  • zlend:用於標記壓縮列表的末端。

7、物件

Redis 的物件型別:字串物件、列表物件、雜湊物件、集合物件、有序集合物件。

7.1 物件型別與編碼

Redis 使用物件來表示資料庫中的鍵和值中,每個物件都由一個 redisObject 結構表示:

type struct redisObject {
    // 型別
    unsigned type:4;
    
    // 編碼
    unsigned encoding:4;
    
    // 指向底層實現資料結構的指標
    void *ptr;
    
    //...
} robj;
複製程式碼

通過 encoding 屬性來設定物件所使用的編碼,可以使得 Redis 根據不同的使用場景為一個物件設定不同的編碼,從而優化物件在某一場景下的效率。

舉個例子,在列表物件包含的元素較少的時候,Redis 使用壓縮列表作為列表物件的底層實現:

  • 因為壓縮列表比雙端連結串列更節約記憶體,並且在元素數量較少的時候,在記憶體中以連續塊方式儲存的壓縮列表比起雙端連結串列可以更快被載入到快取中;
  • 隨著列表物件包含的元素越來越多,使用壓縮列表來儲存元素的優勢逐漸消失時,物件就會將底層實現從壓縮列表轉向功能更強、也更適合儲存大量元素的雙端連結串列。

7.2 字串物件

字串物件的編碼可以是 int,raw 或者 embstr。

  • 如果一個字串物件儲存的是整數值,並且這個整數值可以用 long 型別來表示,那麼字串物件會將整數值儲存在字串物件結構的ptr屬性裡面(將 void* 轉換成 long),並將字串的編碼設定為 int。
  • 如果字串物件儲存的是一個字串值,並且這個字串值的長度大於 32 位元組,那麼字串物件將使用一個簡單動態字串 (SDS) 來儲存這個字串值,並將物件的編碼設定為 raw。
  • 如果字串物件儲存的是一個字串值,並且這個字串值的長度小於等於 32 位元組,那麼字串物件將使用 embstr 編碼的方式來儲存這個字串值。
  • Redis 中 longdouble 型別表示的浮點數也是作為字串值來儲存的。

7.3 列表物件

列表物件的編碼可以是 ziplist 或者 linkedlist。

  • Ziplist 編碼的列表物件使用壓縮列表作為底層實現,每個壓縮列表節點 (entry) 儲存一個列表元素。
  • Linkedlist 編碼的列表物件使用雙端連結串列作為底層實現,每個雙端連結串列節點 (node) 都儲存了一個字串物件,每個字串物件都儲存一個列表元素。

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

  • 列表物件儲存的所有字串元素的長度都小於 64 位元組;
  • 列表物件儲存的元素數量小於 512 個;不能滿足這兩個條件的列表物件需要使用linkedlist編碼。

7.4 雜湊物件

雜湊物件的編碼可以是 ziplist 或者 hashtable。

  • ziplist 編碼的雜湊物件使用壓縮表作為底層實現。儲存同一鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後;先新增的在表頭方向,後新增的在表尾方向。
  • hashtable 編碼的雜湊物件使用字典作為底層實現,雜湊物件中的每個鍵值對都使用一個字典鍵值對來儲存:字典的每個鍵都是一個字串物件,物件中儲存了鍵值對的鍵;字典中每個值都是字串物件,物件中儲存了鍵值對的值

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

  • 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於 64 位元組;
  • 雜湊物件儲存的鍵值對數量小於 512 個;不能滿足這兩個條件的雜湊物件需要使用 hashtable 編碼。

7.5 集合物件

集合物件的編碼可以是 intset 或者 hashtable。

  • inset編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合裡面。
  • hashtable編碼的集合物件使用字典作為底層實現,字典的每個鍵都是一個字串物件,每個字串物件包含一個集合元素,而字典的值則全部被設定為NULL。

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

  • 集合物件儲存的所有元素都是整數值;
  • 集合物件儲存的元素數量不超過512個;不能滿足這兩個條件的集合物件需要使用hashtable編碼。

7.6 有序集合物件

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

  • ziplist 編碼的壓縮列表物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一個的壓縮列表節點來儲存,第一個節點儲存元素的成員 (member),第二個節點儲存元素的分值 (score)。壓縮列表內的元素按分值從小到大進行排序,分值較小的在表頭方向,較大的在表尾方向。
  • skiplist 編碼的有序集合物件使用 zset 結構作為底層實現,一個 zset 結構同時包含一個字典和一個跳躍表。

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

  • 有序集合儲存的元素數量小於128個;
  • 有序集合儲存的所有元素成員的長度都小於64位元組;不能滿足以上兩個條件的有序集合物件將使用skiplist編碼。

7.7 檢查型別

任何型別的鍵都可以執行的命令:DEL、EXPIRE、RENAME、TYPE、OBJECT

只能對特定型別的鍵執行:

在這裡插入圖片描述
DEL、EXPIRE 等命令和 LLEN 等命令的區別在於,前者是基於型別的多型,即一個命令可以同時處理多種不同型別的鍵;而後者是基於編碼的多型,即一個命令可以同時處理多種不同編碼。

7.8記憶體回收

Redis 構建了一個引用計數 (reference counting) 技術實現的記憶體回收機制。

typedef struct redisObject{
    
    //...
    //引用計數
    Int refcount;
    //...
}robj;
複製程式碼

物件的引用計數資訊會隨著物件的使用狀態而不斷變化:

  • 在建立一個新物件時,引用計數的值會被初始化為1;
  • 當一個物件被一個新程式使用時,它的引用計數值會被增一;
  • 當物件不再被一個程式使用時,它的引用計數值會被減一;
  • 當物件的引用計數值變為0時,物件所佔用的記憶體會被釋放。

7.9 物件共享

在Redis中,讓多個鍵共享同一個值物件需要執行以下兩個步驟為:

  • 將資料庫鍵的值指標指向一個現有的值物件;
  • 將被共享的值物件的引用計數增一。

這些共享物件不單單隻有字串鍵可以使用,那些在資料結構中巢狀了字串物件的物件(linklist編碼的列表物件、hashtable編碼的雜湊物件、hashtable編碼的集合物件,以及zset編碼的有序集合物件)都可以使用這些共享物件。

相關文章