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

各位看官大大們,週末好!html

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

Redis是C語音編寫的基於內存的數據結構存儲系統。它能夠看成數據庫、緩存和消息中間件。 它支持多種類型的數據結構,如 字符串(strings),
列表(lists), 字典(dictht)集合(sets), 有序集合(sorted sets)。一般咱們在項目中能夠用它來作緩存、記錄簽到數據、分佈式鎖等等。
要使用Redis首先咱們來了解一下它的五種基礎數據結構。node


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

第二種是本身封裝了一種名爲簡單動態字符串(simple dynamic string)簡稱SDS的抽象類型,這個纔是咱們存儲字符串時所使用的對象,等同於java中的StringBuilder。redis

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等還等待着咱們去探索。

因爲篇幅有限,這期就先到這裏,預知後事如何,請聽下集分解...

 

下集來了,本系列其餘文章:

 小白也能看懂的REDIS教學基礎篇——朋友面試被SKIPLIST跳躍表攔住了

 

 

參考書籍​:

《Reids設計與實現》

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

相關文章
相關標籤/搜索