1. 程式人生 > >小白也能看懂的Redis教學基礎篇——redis基礎資料結構

小白也能看懂的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深度歷險——核心原理與應用實踐》

&n