1. 程式人生 > 其它 >Redis(一):Redis的5種資料型別

Redis(一):Redis的5種資料型別

技術標籤:Redis

1、字串物件

2、連結串列物件

3、雜湊物件

4、集合物件

5、有序集合物件


Redis總共有5種資料型別,分別是字串物件,連結串列物件,雜湊物件,集合物件,有序集合物件。Redis是一個鍵值對資料庫,Redis的鍵總是一個字串物件,Redis的值可以為連結串列、雜湊、集合、有序集合物件等。

1、字串物件

Redis自己構建了一種名為簡單動態字串(SDS)的抽象型別,SDS定義在sds.h中,SDS的各種API在sds.c中實現。

/*
 * 儲存字串物件的結構
 */
struct sdshdr {
    
    // buf 中已佔用空間的長度
    int len;

    // buf 中剩餘可用空間的長度
    int free;

    // 資料空間
    char buf[];
};

SDS與C字串的區別:

  • 常數複雜度獲取字串的長度,SDS的字串長度可以通過len屬性獲取。
  • 杜絕緩衝區溢位,SDS的API需要對SDS進行修改時,會檢查SDS的空間是否滿足修改所需的要求,如果不滿足,API會自動將SDS空間擴充套件至執行修改所需的大小。
  • 減少修改字串帶來的記憶體重新分配次數:

1)通過空間預分配,可以減少字串拼接時記憶體重分配的次數。當SDS的長度小於1MB時,分配兩倍和len屬性同樣大小的未使用空間。如果SDS的長度大於1MB時,將分配1MB的未使用空間。

2)通過惰性空間釋放,可以減少字串縮短時的記憶體回收的次數。SDS提供了相應的API,讓我們在有需要時,真正釋放SDS的未使用空間。

  • 二進位制安全,SDS的API都會以二進位制的方式來處理buf陣列中的內容,因此SDS不僅可以儲存文字,還可以儲存二進位制。而對於C的字串來說,遇到空格就終止了。

2、連結串列物件

連結串列內建在許多高階語言中,如C++,但是Redis使用C語言編寫,沒有內建這種資料結構,所以Redis構建了自己的連結串列實現。Redis的連結串列和連結串列節點定義在adlist.h中,連結串列的API函式在adlist.c中實現。

typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *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;

dup、free和match成員是用於實現多型連結串列所需的型別特定函式。Redis的連結串列實現特性可以總結如下:

  • 雙端:連結串列節點帶有prev和next指標
  • 無環
  • 帶表頭指標和表尾指標
  • 帶連結串列長度計數器
  • 多型 :連結串列節點使用void *指標來儲存節點值,並且可以通過list結構的dup、free和match三個屬性為節點值設定型別特定函式,所以連結串列可以用於儲存各種不同型別的值。

3、雜湊物件

字典經常作為一種資料結構內建在高階程式語言中,如C++,但是Redis使用C語言編寫,並沒有內建這種資料結構,因此Redis構建了自己的字典實現。字典物件的定義在dict.h中,字典物件的API實現在dict.c中。

/*
 * 雜湊表
 *
 * 每個字典都使用兩個雜湊表,從而實現漸進式 rehash 。
 */
typedef struct dictht {
    
    // 雜湊表陣列
    dictEntry **table;

    // 雜湊表大小
    unsigned long size;
    
    // 雜湊表大小掩碼,用於計算索引值
    // 總是等於 size - 1
    unsigned long sizemask;

    // 該雜湊表已有節點的數量
    unsigned long used;

} dictht;

/*
 * 雜湊表節點
 */
typedef struct dictEntry {
    
    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下個雜湊表節點,形成連結串列
    struct dictEntry *next;

} dictEntry;

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

  • dictht 中size屬性記錄了雜湊表的大小,也即是table陣列的大小,而used屬性則記錄了雜湊表目前已有節點的數量。
  • dictEntry中key屬性儲存鍵值對的鍵,v屬性儲存鍵值對的值,鍵值對的值可以是一個指標,或者是一個uint64_t整數,又或者是int64_t整數。
/*
 * 字典
 */
typedef struct dict {

    // 型別特定函式
    dictType *type;

    // 私有資料
    void *privdata;

    // 雜湊表
    dictht ht[2];

    // rehash 索引
    // 當 rehash 不在進行時,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在執行的安全迭代器的數量
    int iterators; /* number of iterators currently running */

} 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;
  • dict中type屬性是一個指向dictType結構的指標,每個dictType結構儲存了一簇用於操作特性型別鍵值對的函式,Redis會為不同的字典設定不同的型別特定函式。
  • dict中的privdata屬性則儲存了需要傳給那些型別特定函式的可選引數。
  • ht陣列中每個項都是一個dictht雜湊表,一般情況下,字典只使用ht[0]雜湊表,ht[1]雜湊表只會在對ht[0]雜湊表進行rehash時使用。
  • rehashidx記錄了rehash目前的進度,如果目前沒有進行rehash,那麼它的值為-1。

(1) 雜湊演算法:

第一步,計算雜湊值:hash = dict->type->hashFunction(key);

第二步,根據雜湊值和sizemask的值計算索引值:index = hash & dict->ht[x].sizemask;

(2)解決衝突:雜湊表使用鏈地址法解決衝突

(3)rehash:為了讓雜湊表的負載因子(load_factor)維持在一個合理的範圍內,當雜湊表儲存的鍵值對數量太多或者太少時,程式需要對雜湊表的大小進行相應的擴充套件或者收縮。

load_factor = ht[0].used / ht[0].size

當一下條件中的任意一個被滿足時,程式會自動開始對雜湊表執行擴充套件操作:

  • 伺服器目前沒有執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1.
  • 伺服器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5.
  • 當雜湊表的負載因子小於0.1時,程式自動開始對雜湊表執行收縮操作。

rehash的操作步驟如下:

  • 為字典ht[1]雜湊表分配空間,這個空間大小取決於要執行的操作,以及ht[0]當前包含的鍵值對數量。
  • 將儲存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash是指重新計算鍵的雜湊值和索引,然後將鍵值對放在ht[1]雜湊表指定的位置上。
  • ht[0]全部遷移到ht[1]之後,釋放ht[0],將ht[1]設定為ht[0],並在ht[1]新建立一個空白雜湊表。

(4)漸進式rehash:ht[0]的鍵值對rehash到ht[1]的動作不是一次性、集中式的完成,而是分多次、漸進式的完成的。這是因為如果一個雜湊表儲存的鍵值對過多,一次性rehash會導致伺服器在一段時間內停止服務。漸進式rehash的步驟如下:

  • 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]
  • 把字典中的rehashidx設定為0,表示rehash工作正式開始
  • 在rehash進行期間,每次對字典執行新增、刪除、查詢或者更新操作時,程式除了執行的操作外,還會順帶將ht[0]雜湊表在rehashidx索引上的所有鍵值對rehash到ht[1],當rehash工作完成後,程式將rehashidx屬性的值增一。
  • 隨著字典操作的不斷執行,最終ht[0]的所有鍵值對都會rehash到ht[1],這時程式將rehashidx的值設定為-1,表示rehash完成。
  • 在漸進式rehash的過程中,字典的刪除、查詢、更新等操作會在兩個雜湊表上執行。新新增到字典的鍵值對一律會被儲存到ht[1]裡面,而ht[0]則不再進行任何新增操作。這一措施保證了ht[0]所包含的鍵值對數量只減不增,並且最終變成空表。

4、集合物件

整數集合intset是Redis用於儲存整數值的集合抽象資料型別,它可以儲存型別為int16_t、int32_t、int64_t的整數值,並且保證集合中不會出現重複元素。整數集合定義在intset.h中,整數集合的API實現在intset.c中。

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

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

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

} intset;
  • intset中length記錄了整數集合包含的元素的數量,也是contents陣列的長度。雖然contents屬性宣告為int8_t型別的陣列,但實際上contents陣列並不儲存任何int8_t型別的值,contents陣列的真正型別取決於encoding屬性的值。encoding可以取值為:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64。
  • 升級:當我們將一個新元素新增到整數集合裡面,並且新元素型別比整數集合現有的所有元素型別都要長時,整數集合需要先進行升級,然後才能將新元素新增到整數集合裡面。升級提升了靈活性並且節約了記憶體,整數集合不支援降級操作。升級分為三步:

1) 根據新元素型別,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間。

2) 轉換原有元素的資料型別,並將轉換後的元素放在正確的位置上,放置的過程中需要維持底層陣列的有序性質不變。

3) 將新元素新增到底層數組裡面。

5、有序集合物件

跳躍表是一種有序資料結構,支援平均O(logN)、最壞O(N)複雜度的節點查詢,在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹更簡單,所以不少程式使用跳躍表來代替平衡樹。跳躍表的實現定義在redis.h中,實現在redis.c中。

/*
 * 跳躍表節點
 */
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 中 header指向跳躍表頭節點,tail指向跳躍表尾節點,level記錄了目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內),length記錄了跳躍表的長度,也即是跳躍表目前包含的節點數量。
  • zskiplistNode包含了4個屬性,分別是層(level)、後退(backward)、分值(score)、成員物件(obj):