淺談 Redis 資料結構

今晚打老虎嗎發表於2019-04-21

前言

Redis 資料庫裡面的每個鍵值對都是由物件組成的,其中資料庫的鍵總是一個字串物件(string object),資料庫的值則可以使字串物件、列表物件(list object)、雜湊物件(hash object)、集合物件(set object)和有序集合物件(sorted object)這五種資料結構。下面我們一起來看下這些資料物件在 Redis 的內部是怎麼實現的,以及 Redis 是怎麼選擇合適的資料結構進行儲存等。

簡單動態字串

Redis 沒有直接使用 C 語言傳統的字串標識,而是自己構建了一種名為簡單動態字串 SDS(simple dynamic string)的抽象型別,並將 SDS 作為 Redis 的預設字串。
SDS 結構(如果沒有特殊說明,程式碼採用的一律為 Redis 5.0 版本)

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
複製程式碼
  • len 表示 SDS 的長度,使我們在獲取字串長度的時候可以在 O(1)情況下拿到,而不是像 C 那樣需要遍歷一遍字串。
  • alloc 可以用來計算 free 就是字串已經分配的未使用的空間,有了這個值就可以引入預分配空間的演算法了,而不需要使用者去考慮記憶體分配的問題。預分配在這個字串物件記憶體小於 1M 的時候分配和 len 同樣大小的記憶體,大於 1M 的時候分配 1M記憶體。
  • buf 表示字串陣列

SDS 有五種長度,分別為sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中從不使用sdshdr5,只是直接訪問flags位元組用的。
Redis 的字串結構並沒有拋棄 C字串,這意味著它可以向下相容 C 風格的字串,可以重用 C 字串函式。

連結串列

連結串列提供了高效的節點排重能力,以及順序性的節點訪問方式,而且可以通過增加節點來靈活地調整連結串列的長度。它是一種常用的資料結構,被內建在很多高階語言中。因為C語言並沒有內建這種資料結構,所以 Redis 構建了自己的連結串列實現。
連結串列的節點

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
複製程式碼
  • prev 前置節點
  • next 後置節點
  • value 節點的值 多個 listNode 結構組成一個連結串列;
typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;
複製程式碼
  • head 節點頭指標
  • tail 節點尾指標
  • dup 用於複製連結串列節點所儲存的值
  • free 用於釋放連結串列節點所儲存的值
  • match 用於對比連結串列節點所儲存的值和另一個輸入的值是否相等
  • len 連結串列計數器

Redis 連結串列的特性;

  • 雙端;有 prev 和 next 獲取某個節點的前置和後置都是 O(1)
  • 無環;頭結點的 prev 和尾節點的 next 都指向 NULL
  • 連結串列計數器;獲取連結串列的長度為 O(1)
  • 多型;連結串列節點使用 void* 指標來儲存節點的值,所以連結串列可以用於儲存各種不同型別的值。

字典

字典中一個鍵(key)可以和一個值關聯(value),這種關聯的鍵和值我們稱之為鍵值對。所以字典的每個鍵都是獨一無二的,我們可以根據鍵在 O(1) 的時間複雜度下找到與之相關聯的值。字典也是很多高階語言都內建的一種資料結構,但是 C語言並沒有內建這種資料結構,因此 Redis 自己構建了字典的實現。

字典的內部是採用的雜湊表結構:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
複製程式碼
  • dictEntry 雜湊表陣列
  • size 雜湊表大小
  • sizemask 雜湊表大小掩碼
  • used 雜湊表已有節點的數量

其中 table 是一個陣列,陣列中的每個元素都是指向 dictEntry 的指標。

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry
複製程式碼
  • val 鍵
  • union 值
  • dictEntry 指向下個雜湊表節點

雜湊表在新增一個新的鍵值對的時候,程式會根據鍵值對的鍵計算出雜湊索引值,然後根據索引值將包含鍵值對的雜湊表節點放到雜湊表陣列的指定索引上。
當有兩個或者以上的鍵被分配到雜湊表陣列的同一個索引上面時,會產生衝突。 Redis 的雜湊表使用鏈地址法(separate chaining)來解決建衝突。
隨著不斷的執行,雜湊表儲存的鍵值對隨之也會做多或者減少,為了讓雜湊表的負載因子維持在一個合理範圍內,所以程式需要對雜湊表的大小進行相應的擴充套件或者收縮。執行原理類似動態陣列。當空間不夠或者剩餘的時候自動申請一塊記憶體空間進行資料轉移,在 Redis 中叫做 rehash。

跳躍表

跳躍表是一種有序的鏈性資料結構,通過維護層級 (level) 來達到快速訪問節點的目的。平均查詢複雜度為 O(logN),最壞 O(N)。因為是鏈性結構,還支援順序性操作。
關於 Redis 為什麼採用跳躍表而不採用紅黑樹之前我寫過一篇文章,所以就不在這細訴了,我覺得其主要原因不外乎兩點,一是紅黑樹不易於實現,而且在頻繁的新增修改之後,為了維持樹的平衡還要進行左右旋轉。二是紅黑樹查詢雖然是 O(logN),但是在進行區間查詢中往往就做到不 O(logN) 了,甚至需要遍歷整個樹。跳錶就不需要了,它只需要找到第一個節點然後根據鏈性結構的特點向下走就可以了。Redis 有序集合一般就是用的這種實現。

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

複製程式碼

zskiplistNode 為跳躍表節點:

  • sds 元素
  • score 分值
  • backward 後退指標
  • level 層級

zskiplist 為跳躍表:

  • header 頭結點
  • tail 尾節點
  • length 節點數量
  • level 最大節點的層數

整數集合

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

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
複製程式碼
  • encoding 編碼方式
  • length 集合元素個數
  • contents 儲存集合資料的陣列

當集合資料型別大於 int8_t 所表示的最大空間時,Redis 會自動為該集合升級。一旦升級,不支援降級。

壓縮列表

壓縮列表是一種為節約記憶體而開發的順序性資料結構。常常被用作列表、雜湊的底層實現。是由一系列特殊編碼的連續記憶體塊組成的。

總結

Redis 內部是由一系列物件組成的,字串物件、列表物件、雜湊表物件、集合物件有序集合物件。
字串物件是唯一一個可以應用在上面所以物件中的,所以我們看到向一些 keys exprice 這種命令可以在針對所有 key 使用,因為所有 key 都是採用的字串物件。 列表物件預設使用壓縮列表為底層實現,當物件儲存的元素數量大於 512 個或者是長度大於64位元組的時候會轉換為雙端連結串列。
雜湊物件也是優先使用壓縮列表鍵值對在壓縮列表中連續儲存著,當物件儲存的元素數量大於 512 個或者是長度大於64位元組的時候會轉換為雜湊表。
集合物件可以採用整數集合或者雜湊表,當物件儲存的元素數量大於 512 個或者是有元素非整數的時候轉換為雜湊表。
有序集合預設採用壓縮列表,當集合元素數量大於 128 個或者是元素成員長度大於 64 位元組的時候轉換為跳躍表。

參考資料

Redis 設計與實現
Redis in Action

相關文章