Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表

犀利豆發表於2018-03-14

原文地址:www.xilidou.com/2018/03/13/…

上篇文章寫了 Redis 基礎資料結構的可變字串、連結串列、字典。大家可以點選連結檢視。今天我們繼續研究 Redis 的基礎資料結構。

  • 整數集合
  • 跳躍表
  • 壓縮列表

整數集合

當一個集合只包含整數,且這個集合的元素不多的時候,Redis 就會使用整數集合 intset 。首先看 intset 的資料結構:

typedef struct intset {
    
    // 編碼方式
    uint32_t encoding;

    // 集合包含的元素數量
    uint32_t length;

    // 儲存元素的陣列
    int8_t contents[];
} intset;

複製程式碼

其實 intset 的資料結構比較好理解。一個資料儲存元素,length 儲存元素的數量,也就是contents的大小,encoding 用於儲存資料的編碼方式。

通過程式碼我們可以知道,encoding 的編碼型別包括了:

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
複製程式碼

實際上我們可以看出來。 Redis encoding的型別,就是指資料的大小。作為一個記憶體資料庫,採用這種設計就是為了節約記憶體。對於一個資料我們可以畫一個圖來幫助理解:

既然有從小到大的三個資料結構,在插入資料的時候儘可能使用小的資料結構來節約記憶體,如果插入的資料大於原有的資料結構,就會觸發擴容。

擴容有三個步驟:

  1. 根據新元素的型別,修改整個陣列的資料型別,並重新分配空間
  2. 將原有的的資料,裝換為新的資料型別,重新放到應該在的位置上,且儲存順序性
  3. 再插入新元素

整數集合不支援降級操作,一旦升級就不能降級了。

跳躍表

跳躍表是連結串列的一種,是一種利用空間換時間的資料結構。跳錶平均支援 O(logN),最壞O(N)複雜度的查詢。

跳錶是由一個zskiplist 和 多個 zskiplistNode 組成。我們先看看他們的結構:


/* ZSETs use a specialized version of Skiplists */
/*
 * 跳躍表節點
 */
typedef struct zskiplistNode {

    // 成員物件
    robj *obj;

    // 分值
    double score;

    // 後退指標
    struct zskiplistNode *backward;

    // 層
    struct zskiplistLevel {

        // 前進指標
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

/*
 * 跳躍表
 */
typedef struct zskiplist {

    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;

    // 表中節點的數量
    unsigned long length;

    // 表中層數最大的節點的層數
    int level;

} zskiplist;

複製程式碼

所以根據這個程式碼我們可以畫出如下的結構圖:

zskiplist

其實跳錶就是一個利用空間換時間的資料結構,利用 level 作為連結串列的索引。

之前有人問過 Redis 的作者 為什麼使用跳躍表,而不是 tree 來構建索引?作者的回答是:

  1. 省記憶體。
  2. 服務於 ZRANGE 或者 ZREVRANGE 是一個典型的連結串列場景。時間複雜度的表現和平衡樹差不多。
  3. 最重要的一點是跳躍表的實現很簡單就能達到 O(logN)的級別。

壓縮列表

壓縮連結串列 Redis 作者的介紹是,為了儘可能節約記憶體設計出來的雙向連結串列。

對於一個壓縮列表程式碼裡註釋給出的資料結構如下:

ziplist

zlbytes 表示的是整個壓縮列表使用的記憶體位元組數

zltail 指定了壓縮列表的尾節點的偏移量

zllen 是壓縮列表 entry 的數量

entry 就是 ziplist 的節點

zlend 標記壓縮列表的末端

這個列表中還有單個指標:

ZIPLIST_ENTRY_HEAD 列表開始節點的頭偏移量

ZIPLIST_ENTRY_TAIL 列表結束節點的頭偏移量

ZIPLIST_ENTRY_END 列表的尾節點結束的偏移量

再看看一個 entry 的結構:


/*
 * 儲存 ziplist 節點資訊的結構
 */
typedef struct zlentry {

    // prevrawlen :前置節點的長度
    // prevrawlensize :編碼 prevrawlen 所需的位元組大小
    unsigned int prevrawlensize, prevrawlen;

    // len :當前節點值的長度
    // lensize :編碼 len 所需的位元組大小
    unsigned int lensize, len;

    // 當前節點 header 的大小
    // 等於 prevrawlensize + lensize
    unsigned int headersize;

    // 當前節點值所使用的編碼型別
    unsigned char encoding;

    // 指向當前節點的指標
    unsigned char *p;

} zlentry;

複製程式碼

依次解釋一下這幾個引數。

prevrawlen 前置節點的長度,這裡多了一個 size,其實是記錄了 prevrawlen 的尺寸。Redis 為了節約記憶體並不是直接使用預設的 int 的長度,而是逐漸升級的。 同理 len 記錄的是當前節點的長度,lensize 記錄的是 len 的長度。 headersize 就是前文提到的兩個 size 之和。 encoding 就是這個節點的資料型別。這裡注意一下 encoding 的型別只包括整數和字串。 p 節點的指標,不用過多的解釋。

需要注意一點,因為每個節點都儲存了前一個節點的長度,如果發生了更新或者刪除節點,則這個節點之後的資料也需要修改,有一種最壞的情況就是如果每個節點都處於需要擴容的零界點,就會造成這個節點之後的節點都要修改 size 這個引數,引發連鎖反應。這個時候就是 壓縮連結串列最壞的時間複雜度 O(n^2)。不過所有節點都處於臨界值,這樣的概率可以說比較小。

總結

至此Redis的基本資料結構就介紹完了。我們可以看到 Redis 對記憶體的使用真是“斤斤計較”,對於記憶體是使用特別節約。同時 Redis 作為一個單執行緒應用,不用考慮併發的問題,將很多類似 size 或者 length 的引數暴露出來,將很多 O(n) 的操作降低為 O(1)。大大提升效率。下一講,將會介紹 Redis 是怎麼通過這些資料結構向外提供服務。 Redis 的程式碼真是寫的太棒了,簡潔高效。值得大家學習。

歡迎關注我的微信公眾號:

二維碼

相關文章