淺談 Redis 數據結構

前言

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 有五種長度,分別爲sdshdr五、sdshdr八、sdshdr1六、sdshdr3二、sdshdr64。其中從不使用sdshdr5,只是直接訪問flags字節用的。
Redis 的字符串結構並無拋棄 C字符串,這意味着它能夠向下兼容 C 風格的字符串,能夠重用 C 字符串函數。數組

鏈表

鏈表提供了高效的節點排重能力,以及順序性的節點訪問方式,並且能夠經過增長節點來靈活地調整鏈表的長度。它是一種經常使用的數據結構,被內置在不少高級語言中。由於C語言並無內置這種數據結構,因此 Redis 構建了本身的鏈表實現。
鏈表的節點bash

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 本身構建了字典的實現。函數

字典的內部是採用的哈希表結構:ui

/* 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 的指針。this

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 有序集合通常就是用的這種實現。spa

/* 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

相關文章
相關標籤/搜索