Redis基礎(一)資料結構與資料型別

大雜草發表於2020-10-26

Redis資料結構

Redis一共有六種資料結構,分別是簡單動態字串、連結串列、字典、跳錶、整數集合、壓縮列表。

簡單動態字串(SDS)

Redis只會使用C字串作為字面量,在大多數情況下,Redis使用SDS(Simple Dynamic String,簡單動態字串)作為字串表示。

SDS的資料結構:

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

image

比起C字串,SDS具有以下優點:

  • 常數複雜度獲取字串長度
  • 杜絕緩衝區溢位
  • 減少修改字串時帶來的記憶體重分配次數
  • 二進行安全
  • 相容部分C字串函式

連結串列(list)

連結串列的資料結構:

typedef struct listNode {
    // 前置節點
    struct listNode *prev;
    
    // 後置節點
    struct listNode *next;
    
    // 節點的值
    void *value;
}listNode;

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;

image

  • 連結串列被廣泛用於實現Redis的各種功能,比如列表鍵、釋出與訂閱、慢查詢、監視器等。
  • 每個連結串列節點由一個listNode結構來表示,每個節點都有一個指向前置節點和後置節點的指標,所以Redis的連結串列實現是雙端連結串列。
  • 每個連結串列使用一個list結構來表示,這個結構帶有表頭節點指標、表尾節點指標,以及連結串列長度等資訊。
  • 因為連結串列表頭節點的前置節點和表尾節點的後置節點都指向NULL,所以Redis的連結串列實現是無環連結串列。
  • 通過為連結串列設定不同的型別特定函式,Redis的連結串列可以用於儲存各種不同型別的值。

字典(dict)

字典的資料結構:

typedef struct dictht {
    // 雜湊表陣列
    dictEntry **table;
    
    // 雜湊表大小
    unsigned long size;
    
    // 雜湊表大小掩碼,用於計算索引值
    unsigned long sizemask;
    
    // 該雜湊表已有節點的數量
    unsigned long used;
}dictht;

typedef struct dictEntry {
    // 鍵
    void *key;
    
    // 值
    union {
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    
    // 指向下一個雜湊表節點,形成鍵表
    struct dictEntry *next;
}dictEntry;

typedef struct dict {
    // 型別特定函式
    dictType *type;
    
    // 私有資料
    void *privdate;
    
    // 雜湊表
    dictht ht[2];
    
    // rehash索引
    // 當rehash不在進行時,值為-1
    in trehashidx; /* rehashing not in progress if rehashidx == -1 */
}dict;

typedef struct dictType {
    // 計算雜湊值的函式
    unsigned int (*hashFunction)(const void *key);
    
    // 複製鍵的函式
    void *(*keyDup)(void *privdata, const void *key);
    
    // 複製值的函式
    void *(*valDup)(void *privdata, const void *obj);
    
    // 對比鍵的函式
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    
    // 銷燬鍵的函式
    void (*keyDestructor)(void *privdata, void *key);
    
    // 銷燬值的函式
    void (*valDestructor)(void *privdata, void *obj);
}dictType;

image

  • 字典被廣泛用於實現Redis的各種功能,其中包括資料庫和雜湊鍵。
  • Redis中的字典使用雜湊表作為底層實現,每個字典帶有兩個雜湊表,一個平時使用,另一個僅在進行rehash時使用。
  • 當字典被用作資料庫的底層實現,或者雜湊鍵的底層實現時,Redis使用MurmurHash2演算法來計算鍵的雜湊值。
  • 雜湊表使用鍵地址法來解決鍵衝突,被分配到同一個索引上的多個鍵值對會連線成一個單向連結串列。
  • 在對雜湊表進行擴充套件或者收縮操作時,程式需要將現有雜湊表包含的所有鍵值對rehash到新雜湊表裡面,並且這個rehash過程並不是一次性地完成的,而是漸進式地完成的。

跳錶(skiplist)

跳錶(skiplist)是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。跳錶查詢的時間複雜度O(logN)、最壞情況是O(N),還可以通過順序操作來指處理節點。

跳錶的資料結構:

typedef struct zskiplistNode {
    // 層
    struct zskiplistLevel {
        // 前進指標
        struct zskiplistNode * forward;
        
        // 跨度
        unsigned int span;
    } level[];
    
    // 後退指標
    struct zskiplistNode *backward;
    
    // 分值
    double score;
    
    // 成員物件
    robj *obj;
} zskiplistNode;

typedef struct zskiplist {
    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;
    
    // 表中節點數量
    unsigned long length;
    
    // 表中層數最大的節點的層數
    int level;
} zskiplist;

image

  • 跳錶是有序集合的底層實現之一。
  • Redis的跳錶實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於儲存跳錶資訊(比如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳錶節點。
  • 每個跳錶節點的層高都是1至32之間的隨機數。
  • 在同一個跳錶中,多個節點可以包含相同的分值,但每個節點的成員物件必須是唯一的。
  • 跳錶中的節點按照分值大小進行排序,當分值相同時,節點按照成員物件的大小進行排序。

整數集合(intset)

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

整數集合的資料結構:

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

image

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

壓縮列表(ziplist)

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

image

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

Redis資料型別

Redis中,鍵的資料型別是字串,但提供了豐富的資料儲存方式,方便開發者使用,值的資料型別有很多,常用的資料型別有五種,分別是字串(string)、列表(list)、字典(hash)、集合(set)、有序集合(sortedset)。

字串(string)

“字串(string)”這種資料結構型別非常簡單,對應到資料結構裡,就是Redis裡的簡單動態字串(SDS)。

列表(list)

列表這種資料型別支援儲存一組資料。這種資料型別對應兩種實現方法,一種是壓縮列表(ziplist),另一種是雙向迴圈連結串列。

當列表中儲存的資料量比較小的時候,列表就可以採用壓縮列表的方式實現。具體需要同時滿足下面兩個條件:

  • 列表中儲存的單個資料(有可能是字串型別的)小於64位元組;
  • 列表中資料個數少於512。

字典(hash)

字典型別用來儲存一組資料對。每個資料對又包含鍵值兩部分。字典型別也有兩種實現方式。一種是壓縮列表,另一種是雜湊表。

同樣,只有當儲存的資料量比較小的情況下,Redis才使用壓縮列表來實現字典型別。具體需要滿足兩個條件:

  • 字典中儲存的鍵和值的大小都要小於64位元組;
  • 字典中鍵值對的個數要小於512個。

集合(set)

集合這種資料型別用來儲存一組不重複的資料。有兩種實現方式:一種是基於有序陣列,另一種是基於雜湊表。

當要儲存的資料,同時滿足下面這樣兩個條件的時候,Redis就採用有序陣列,來實現集合這種資料型別。

  • 儲存的資料都是整數;
  • 儲存的資料元素個數不超過512個。

有序集合(sortedset)

有序集合用來儲存一組資料,並且每個資料會附帶一個得分。通過得分的大小,我們將資料組織成跳錶這種資料結構,以支援快速地按照得分值、得分割槽間獲得資料。

有序集合也有兩種實現方式:跳錶和壓縮列表。使用壓縮列表來實現有序集合的前提:

  • 所有資料的大小都要小於64位元組;
  • 元素個數要小於128個。

參考資料

相關文章