小白也能看懂的Redis教學基礎篇——redis基礎數據結構

各位看官大大們,周末好!

作為一個Java後端開發,要想獲得比較可觀的工資,Redis基本上是必會的(不要問我為什麼知道,問就是被問過無數次)。
那麼Redis是什麼,它到底擁有什麼神秘的力量,能獲得眾多公司的青睞?接下來就由小編我帶大家來揭秘Redis的五種基本數據結構。

Redis是C語音編寫的基於記憶體的數據結構存儲系統。它可以當作資料庫、快取和消息中間件。 它支援多種類型的數據結構,如 字元串(strings),
列表(lists), 字典(dictht)集合(sets), 有序集合(sorted sets)。通常我們在項目中可以用它來做快取、記錄簽到數據、分散式鎖等等。
要使用Redis首先我們來了解一下它的五種基礎數據結構。

1.字元串(strings)
Redis擁有兩種字元串表述方式,其一是C語言傳統的字元串表述方式,常用Redis程式碼中字元串常量等一些無需對字元串進行修改的地方。

第二種是自己封裝了一種名為簡單動態字元串(simple dynamic string)簡稱SDS的抽象類型,這個才是我們存儲字元串時所使用的對象,等同於java中的StringBuilder。

SDS的結構如下:

struct sdshdr{
    //記錄字元數組中已經使用的位元組數量 即是字元串的長度
    int len;
    //記錄字元數組中未使用的位元組數
    int free;
    //字元數組 用於保存字元串
    char buf[];
}

結構如下圖所示:

 

至於Redis為什麼要使用這樣的結構,其實和java使用StringBuilder的思維是大相徑庭。為了方便修改和提升性能。比如C的字元串獲取字元串長度時要遍歷整個字元數組。
其時間複雜度是O(n),而SDS則可以直接獲取len,時間複雜度為O(1)。修改字元串N次字元串並且字元串和以前的長度不一致時,C普通字元串長度必然需要執行N次記憶體重分配。
而SDS存在預擴容,所以最多需要執行N次記憶體分配。
註:與擴容其本質和list類似,在需要的長度大於現在數組的長度時,會觸發字元串擴容,當數據小於1M時,字元數組每次擴容都是其原來容量的2倍。1M後每次擴容新增1M容量。

 

2.列表
Redis中的列表相當於java中的LinkedList,它是一個雙向鏈表,插入和刪除都擁有極好的性能,時間複雜度為O(1),但是隨機查找比較慢,時間複雜度為O(n)。雖然可以將列表
當成一個LinkedList,但是在Redis內部列表並不是一個簡單的雙向鏈表的實現。在列表保存元素個數小於512個且每個元素長度小於64位元組的時候為了節省記憶體其底層實現是一塊
連續記憶體來存儲,稱之為ziplist壓縮列表。當不滿足之前的兩個條件時則改用quicklist快速列表來存儲原元素。

ziplist壓縮列表:
壓縮列表是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型數據結構。一個壓縮列表可以包含任意多個節點,每個節點保存一個位元組數組或者一個整數值。

struct ziplist<T>{
    int32 zlbytes;
    int32 zltail_offset;
    int16 zllemhth;
    T[] entries;
    int8 zlend;
}

其結構如下圖所示:

   

節點結構如下:

struct entry{
    int<var> previous_entry_length;//前一個原數的位元組長度
    int<var> encoding;//元數類型編碼
    optional byte[] content;//元素內容
}

這裡有一個點要注意,如果entryX+1和起身後的節點的長度都都在250~253個位元組之間的話,如果entryX長度變成了254個位元組。那麼entryX+1中的previous_entry_length將擴容成5個位元組,

這將導致entryX+1的整體長度也會大於254個位元組,引起entryX+2個位元組中的previous_entry_length也發生擴容,使得entryX+2的整體長度也超過254。並對後面的節點造成連鎖影響這個就叫連鎖更新。

將會對性能造成一定的影響。

 quicklist快速列表:

快速列表是ziplist和linkedlist的混合體。它將linkedlist按段切分,每一段使用ziplist讓記憶體緊湊,多個ziplist之間使用雙向指針串接起來。為了進一步節省空間。Redis還會對ziplist進行壓縮,

使用LZF演算法壓縮。可以選擇壓縮的深度,默認的壓縮深度是0既不壓縮。有時候為了節省空間,但是又不想因為壓縮而影響取出和放入的性能,可以選著壓縮深度為1或者2。

既首尾的第一個或者首尾的第一個和第二個不壓縮。

struct quicklist{
    quicklistNode* head;//頭節點
    quicklistNode* tail;//尾節點
    long count;//元素總數
    int nodes;//ziplist 節點數量
    int compressDepth;//LZF 演算法壓縮深度
};

struct quicklistNode{
    quicklistNode* prev;//前一個節點
    quicklistNode* next;//下一個節點
    ziplist* zl;//指向壓縮列表的指針
    int32 size;//壓縮列表的位元組總數
    int16 count;//壓縮列表中的元素個數
    int2 encoding;//存儲形式 2bit 是原生位元組數組還是被壓縮過的
};

註:LZF演算法是蘋果開源的一種無損壓縮演算法,有興趣的看官們可以自行去了解下,這裡不做過多的贅述。

 

 

3.字典(dictht)

字典又稱之為hash,或者映射(map),也可以理解為redis自己實現的JDK1.7版本的HashMap。是一種用於保存鍵值對的抽象數據結構。在字典中,一個鍵(Key)可以和一個值(value)進行關聯,成為一個鍵值對。

字典中每個鍵都是唯一的。程式可以在字典中根據鍵查找與之關聯的值,或者通過鍵來跟新或者刪除值。接下來的內容將詳細介紹Redis中字典的兩種底層實現。

第一種:ziplist

當字典中的元素滿足以下兩個條件時,字典的底層將會使用ziplist來報錯鍵值對。

1.字典對象保存的所有鍵值對的鍵和值的字元串長度都小於64個位元組。

2.欄位對象保存的鍵值對數量小於512個。

看到這裡也許有的看官會不明白了。在上面我們剛剛學過ziplist壓縮列表,大家都知道這其實就是一個數組。前面的列表可以用數組來保存,但是這裡是鍵值對啊,一個map,怎麼用數組來保存?

各位看官先莫慌,按照慣例先上圖(畢竟無圖無真相啊):

 

第二種:hash表

hash表顧名思義,其本質就是一個HashMap。下面我帶各位看官們看看他的結構

typedef struct dict{
    dictType *type;//類型特定函數
    void *privdata;//私有函數
    dictht ht[2];//hash表
    int trehashidx;//擴容索引 當不在擴容的時候 為-1
};

typedef struct dictht{
    dictEntry **table;//哈希表數組
    unsigned long size;//哈希表大小
    unsigned long sizemask;//哈希表槽位取模基準參數 總是等於size - 1
    unsigned long used;//已有節點數量
}

typedef struct dictEntry{
    void *key;////值 這裡三個屬性是因為 值可能是一個對象引用也可能是 一個uint64_t或者int64_t整數值
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    //下一個節點 多個槽位相同的值 串聯成一個鏈表
    struct dictEntry *next;
}

 結構示意圖:

 

漸進式rehash :

字典在擴容的過程中會在 ht[1] 創建一個新的哈希表,而且它並不會一次性將所有的數據都轉移到新的哈希表之中。而是分而治之,像螞蟻搬家一樣,一部分一部分的遷移,我們稱之為漸進式rehash。

因為篇幅原因,後面會寫一篇專門的文章來詳細說明漸進式rehash和在遷移過程中獲取元素的方式,這裡就簡略的介紹一下。

4.集合(sets)

Redis集合中的Set集合,相當與java中的HashSet,它內部的鍵值對是無序的,唯一的。在Redis中Set集合底層也存在兩種實現方式。

第一種,當一個集合只包含整數值元素,並且這個集合的元素數量不超過512時,Redis就會使用整數集合作為集合鍵的底層實現。

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

contents數組是整數集合的底層實現:整數集合的每個元素都是contents數組的一個數組項(item),各個項在數組中按值的大小從小到大有序地排列,並且數組中不包含任何重複項。

length屬性記錄了整數集合包含的元素數量,也即是contents數組的長度。雖然intset結構將contents數組聲明為int8_t類型的數組。但實際上contents數組的真正類型取決於encoding;

如果encoding類型為INTSET_ENC_INT16,那麼contents就是一個int16_t類型的數組。

如果encoding類型為INTSET_ENC_INT32,那麼contents就是一個int32_t類型的數組。

如果encoding類型為INTSET_ENC_INT64,那麼contents就是一個int64_t類型的數組。

 

整數數組的升級:

當我們要將一個新的元素添加到集合中,並且新元素的類型比整數集合現有的所有元素類型都要長時。整數集合現有先進行升級,然後才能將新元素添加到整數集合里。

比如向一個包含1,2,3 的數組中插入一個65535;

第二種使用字典實現,字典的每個鍵都是一個字元串對象,而值則全部被設置為Null;

 

5.有序集合(ZSet)

 ZSet在Redis底層也存在兩種實現,一種是簡單實現通過Ziplist保存元素成員。

結構如下圖所示:

 

還一種是複雜模型,它內部保存有一個跳錶和一個字典,通過字典來實現O(1)時間複雜度的元素查找,通過跳錶來完成高性能的zrank、zrange等範圍命令。如果單純的字典,要完成zrange命令,

要先將所有數據排序,時間複雜度為O(nlogn),而且還需要長度為N的數組來保存排序完成的數據。如果單純使用跳錶,查詢的時間複雜度為O(logn)。

結構如下圖所示:

 

總結:

這五種只是最常用的五種數據結構,在Redis中還有其他類型的數據結構或者實現。比如緊湊列表listpack,基數樹rax等還等待著我們去探索。

由於篇幅有限,這期就先到這裡,預知後事如何,請聽下集分解…

 

參考書籍​:

 

《Reids設計與實現》

 

​《Redis深度歷險——核心原理與應用實踐》