Redis數據結構底層實現

記得點贊+關注呦。

前言

Redis 有五種基本數據類型,但是你們知道這五種數據類型的底層是咋實現嗎?接下就帶你們瞭解一下 String、List、Hash、Set、Sorted Set 底層是如何實現的,在這以前,先來看下下面的基本數據結構,分別有簡單動態字符串(SDS)、鏈表、字典、跳躍表、整數集合以及壓縮列表,它們是Redis數據結構的基本組成部分。redis

五種數據結構底層實現

1. String算法

  • 若是一個字符串對象保存的是整數值, 而且這個整數值能夠用 long 類型來表示, 那麼字符串對象會將整數值保存在字符串對象結構的 ptr 屬性裏面(將 void* 轉換成 long ), 並將字符串對象的編碼設置爲 int 。
  • 若是字符串對象保存的是一個字符串值, 而且這個字符串值的長度大於 39 字節, 那麼字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值, 並將對象的編碼設置爲 raw。
  • 若是字符串對象保存的是一個字符串值, 而且這個字符串值的長度小於等於 39 字節, 那麼字符串對象將使用 embstr 編碼的方式來保存這個字符串值。

2. List數據庫

  • 列表對象的編碼能夠是 ziplist 或者 linkedlist 。
  • 列表對象保存的全部字符串元素的長度都小於 64 字節而且保存的元素數量小於 512 個,使用 ziplist 編碼;不然使用 linkedlist;

3. Hash數組

  • 哈希對象的編碼能夠是 ziplist 或者 hashtable 。
  • 哈希對象保存的全部鍵值對的鍵和值的字符串長度都小於 64 字節而且保存的鍵值對數量小於 512 個,使用ziplist 編碼;不然使用hashtable;

4. Set服務器

  • 集合對象的編碼能夠是 intset 或者 hashtable 。
  • 集合對象保存的全部元素都是整數值而且保存的元素數量不超過 512 個,使用intset 編碼;不然使用hashtable;

5. Sorted Set微信

  • 有序集合的編碼能夠是 ziplist 或者 skiplist
  • 有序集合保存的元素數量小於 128 個而且保存的全部元素成員的長度都小於 64 字節。使用 ziplist 編碼;不然使用skiplist;

接下來分別說說這些底層數據結構。數據結構

1、簡單動態字符串(SDS)

Redis 本身構建了一種名爲簡單動態字符串(simple dynamic string,SDS)的抽象類型, 並將 SDS 用做 Redis 的默認字符串表示。如:函數

set msg "hello world"

key 和 value 底層都是用 SDS 來實現的。
SDS 的結構:優化

struct sdshdr {

    // 記錄 buf 數組中已使用字節的數量
    // 等於 SDS 所保存字符串的長度
    int len;

    // 記錄 buf 數組中未使用字節的數量
    int free;

    // 字節數組,用於保存字符串
    char buf[];

};
  • free 屬性的值爲 0 , 表示這個 SDS 沒有分配任何未使用空間。
  • len 屬性的值爲 5 , 表示這個 SDS 保存了一個五字節長的字符串。
  • buf 屬性是一個 char 類型的數組, 數組的前五個字節分別保存了 'R' 、 'e' 、 'd' 、 'i' 、 's' 五個字符, 而最後一個字節則保存了空字符 '\0' 。

SDS 與 C 語言字符串比較相近,但擁有更過的優點:ui

1. SDS 獲取字符串長度時間複雜度O(1):由於 SDS 經過 len 字段來存儲長度,使用時直接讀取就能夠;C 語言要想獲取字符串長度須要遍歷整個字符串,時間複雜度O(N)。
2. SDS 能杜絕緩衝區的溢出:由於當 SDS API 要對 SDS 進行修改時,會先檢查 SDS 的空間是否足夠,若是不夠的話 SDS 會自動擴容,So,不會形成緩衝區溢出。而 C 語言則不劇本這個功能。
3. SDS 能減小修改字符串時帶來的內存重分配次數:  
    - 空間預分配:當SDS 擴容時不僅是會增長鬚要的空間大小,還會額外的分配一些未使用的空間。分配的規則是:若是分配後SDS的長度小於 1MB,那麼會分配等於分配後SDS 的大小的未使用空間,簡單說就是,SDS 動態分配後是 16KB,那麼就會多分配 16KB 的未使用空間;若是 小於 1MB,那麼久分配 1MB 的未使用空間。  
    - 惰性空間釋放: 惰性空間釋放用於優化 SDS 的字符串縮短操做:當 SDS 的 API 須要縮短 SDS 保存的字符串時,並不會當即內存重分配來回收多出來的字節,而是用 free 來記錄未使用空間。

2、鏈表

  鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,而且能夠經過增刪節點來靈活地調整鏈表的長度。鏈表在 Redis 中的應用很是普遍,好比 List 的底層實現之一鏈表,當一個 List 包含了數量比較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis 就會使用鏈表做爲 List 的底層實現。除了用做 List 的底層實現以外,發佈與訂閱、慢查詢、監視器等動能也用到了鏈表, Redis 服務器自己還使用鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩衝區。
  每一個鏈表節點使用一個 adlist.h/listNode 結構來表示:

typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *value;

} listNode;

多個 listNode 能夠經過 prev 和 next 指針組成雙端鏈表, 以下圖所示。

雖然僅僅使用多個 listNode 結構就能夠組成鏈表, 但使用 adlist.h/list 來持有鏈表的話, 操做起來會更方便:

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;

list 結構爲鏈表提供了表頭指針 head 、表尾指針 tail , 以及鏈表長度計數器 len , 而 dup 、 free 和 match 成員則是用於實現多態鏈表所需的類型特定函數:

  • dup 函數用於複製鏈表節點所保存的值;
  • free 函數用於釋放鏈表節點所保存的值;
  • match 函數則用於對比鏈表節點所保存的值和另外一個輸入值是否相等。
    下圖是由一個 list 結構和三個 listNode 結構組成的鏈表:

    Redis 的鏈表實現的特性能夠總結以下:
  • 雙端: 鏈表節點帶有 prev 和 next 指針, 獲取某個節點的前置節點和後置節點的複雜度都是 O(1) 。
  • 無環: 表頭節點的 prev 指針和表尾節點的 next 指針都指向 NULL , 對鏈表的訪問以 NULL 爲終點。
  • 帶表頭指針和表尾指針: 經過 list 結構的 head 指針和 tail 指針, 程序獲取鏈表的表頭節點和表尾節點的複雜度爲 O(1) 。
  • 帶鏈表長度計數器: 程序使用 list 結構的 len 屬性來對 list 持有的鏈表節點進行計數, 程序獲取鏈表中節點數量的複雜度爲 O(1) 。
  • 多態: 鏈表節點使用 void* 指針來保存節點值, 而且能夠經過 list 結構的 dup 、 free 、 match 三個屬性爲節點值設置類型特定函數, 因此鏈表能夠用於保存各類不一樣類型的值。

    3、字典

    字典, 又稱符號表(symbol table)、關聯數組(associative array)或者映射(map), 是一種用於保存鍵值對(key-value pair)的抽象數據結構。其中 Key 是惟一的。相似 Java 的 Map。
    字典在 Redis 中主要被應用與:

  • Redis 數據庫底層就是用字典實現的,對數據庫的增、刪、改、查操做都是構建在對字典的操做之上,好比:

    > set msg "hello world"
    OK

    這個就是建立一個 key 爲 "msg",value 爲 "hello world" 的鍵值對,保存在表明數據庫的字典中。

  • 字典仍是哈希鍵的底層實現之一: 當一個哈希鍵包含的鍵值對比較多, 又或者鍵值對中的元素都是比較長的字符串時, Redis 就會使用字典做爲哈希鍵的底層實現。

Redis 的字典使用哈希表做爲底層實現, 一個哈希表裏面能夠有多個哈希表節點, 而每一個哈希表節點就保存了字典中的一個鍵值對。
接下來的三個小節將分別介紹 Redis 的哈希表、哈希表節點、以及字典的實現。

哈希表

Redis 字典所使用的哈希表由 dict.h/dictht 結構定義:

typedef struct dictht {

    // 哈希表數組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩碼,用於計算索引值
    // 老是等於 size - 1
    unsigned long sizemask;

    // 該哈希表已有節點的數量
    unsigned long used;

} dictht;

table 屬性是一個數組, 數組中的每一個元素都是一個指向 dict.h/dictEntry 結構的指針, 每一個 dictEntry 結構保存着一個鍵值對。

size 屬性記錄了哈希表的大小, 也便是 table 數組的大小, 而 used 屬性則記錄了哈希表目前已有節點(鍵值對)的數量。

sizemask 屬性的值老是等於 size - 1 , 這個屬性和哈希值一塊兒決定一個鍵應該被放到 table 數組的哪一個索引上面。

下圖 展現了一個大小爲 4 的空哈希表 (沒有包含任何鍵值對)。

哈希節點

哈希表節點使用 dictEntry 結構表示, 每一個 dictEntry 結構都保存着一個鍵值對:

typedef struct dictEntry {

    // 鍵
    void *key;

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

    // 指向下個哈希表節點,造成鏈表
    struct dictEntry *next;

} dictEntry;

key 屬性保存着鍵值對中的鍵, 而 v 屬性則保存着鍵值對中的值, 其中鍵值對的值能夠是一個指針, 或者是一個 uint64_t 整數, 又或者是一個 int64_t 整數。

next 屬性是指向另外一個哈希表節點的指針, 這個指針能夠將多個哈希值相同的鍵值對鏈接在一次, 以此來解決鍵衝突(collision)的問題。

舉個例子, 下圖就展現瞭如何經過 next 指針, 將兩個索引值相同的鍵 k1 和 k0 鏈接在一塊兒。

字典

Redis 中的字典由 dict.h/dict 結構表示:

typedef struct dict {

    // 類型特定函數
    dictType *type;

    // 私有數據
    void *privdata;

    // 哈希表
    dictht ht[2];

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

} dict;

type 屬性和 privdata 屬性是針對不一樣類型的鍵值對, 爲建立多態字典而設置的:

  • type 屬性是一個指向 dictType 結構的指針, 每一個 dictType 結構保存了一簇用於操做特定類型鍵值對的函數, Redis 會爲用途不一樣的字典設置不一樣的類型特定函數。
  • 而 privdata 屬性則保存了須要傳給那些類型特定函數的可選參數。

ht 屬性是一個包含兩個項的數組, 數組中的每一個項都是一個 dictht 哈希表, 通常狀況下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只會在對 ht[0] 哈希表進行 rehash 時使用。

除了 ht[1] 以外, 另外一個和 rehash 有關的屬性就是 rehashidx : 它記錄了 rehash 目前的進度, 若是目前沒有在進行 rehash , 那麼它的值爲 -1 。

下圖 展現了一個普通狀態下(沒有進行 rehash)的字典:

哈希算法

當將一個新的鍵值對插入到字典中,須要計算索引值,Redis 計算索引值的方法是:

# 使用字典設置的哈希函數,計算鍵 key 的哈希值
hash = dict->type->hashFunction(key);

# 使用哈希表的 sizemask 屬性和哈希值,計算出索引值
# 根據狀況不一樣, ht[x] 能夠是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

相似 Java 的HashMap,計算 key 的 hash 值,而後 hash & (len - 1), 而 Redis 的 sizemask 就是 size - 1。

哈希衝突怎麼辦

當出現 Hash 衝突時,Redis 使用的是 鏈地址法 來解決衝突,鏈地址法就是將衝突的節點構成一個鏈表放在該索引位置上,Redis 採用的是頭插法。解決hash衝突的還有三種方法,分別是:開放定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列)、再哈希法以及創建一個公共溢出區,之後會單獨介紹一些解決hash衝突的四種方法。

rehash

隨着不斷的操做,hash表中的鍵值對可能會增多或減小,爲了讓哈希表的負載因子保持在一個範圍內,須要對 hash表進行擴容或收縮,收縮和擴容的過程就叫 rehash。rehash 過程以下:

  1. 爲字典的 ht[1] 哈希表分配空間, 這個哈希表的空間大小取決於要執行的操做, 以及 ht[0] 當前包含的鍵值對數量 (也便是 ht[0].used 屬性的值)(ht 是字典中的 hash 表,上文有介紹):

    • 若是執行的是擴展操做, 那麼 ht[1] 的大小爲第一個大於等於 ht[0].used * 2 的 2^n (2 的 n 次方冪);
    • 若是執行的是收縮操做, 那麼 ht[1] 的大小爲第一個大於等於 ht[0].used 的 2^n 。
  2. 將保存在 ht[0] 中的全部鍵值對 rehash 到 ht[1] 上面: rehash 指的是從新計算鍵的哈希值和索引值, 而後將鍵值對放置到 ht[1] 哈希表的指定位置上。
  3. 當 ht[0] 包含的全部鍵值對都遷移到了 ht[1] 以後 (ht[0] 變爲空表), 釋放 ht[0] , 將 ht[1] 設置爲 ht[0] , 並在 ht[1] 新建立一個空白哈希表, 爲下一次 rehash 作準備

當如下條件中的任意一個被知足時, 程序會自動開始對哈希表執行擴展操做:

服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於 1 ;
服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於 5 ;
其中哈希表的負載因子能夠經過公式:

# 負載因子 = 哈希表已保存節點數量 / 哈希表大小  
load_factor = ht[0].used / ht[0].size

計算得出。

好比說, 對於一個大小爲 4 , 包含 4 個鍵值對的哈希表來講, 這個哈希表的負載因子爲:

load_factor = 4 / 4 = 1
又好比說, 對於一個大小爲 512 , 包含 256 個鍵值對的哈希表來講, 這個哈希表的負載因子爲:

load_factor = 256 / 512 = 0.5
根據 BGSAVE 命令或 BGREWRITEAOF 命令是否正在執行, 服務器執行擴展操做所需的負載因子並不相同, 這是由於在執行 BGSAVE 命令或 BGREWRITEAOF 命令的過程當中, Redis 須要建立當前服務器進程的子進程, 而大多數操做系統都採用寫時複製(copy-on-write)技術來優化子進程的使用效率, 因此在子進程存在期間, 服務器會提升執行擴展操做所需的負載因子, 從而儘量地避免在子進程存在期間進行哈希表擴展操做, 這能夠避免沒必要要的內存寫入操做, 最大限度地節約內存。

另外一方面, 當哈希表的負載因子小於 0.1 時, 程序自動開始對哈希表執行收縮操做。

漸進式 rehash

rehash 時會將 ht[0] 全部的鍵值對遷移到 ht[1] 中,但這個動做不是一次性的,而是分屢次、漸進式地完成。這樣的所得緣由時:當數據量大的時候一次性遷移會形成服務器在一段時間內定製服務。爲了不發生這樣的事就出現了 漸進式rehash
如下是哈希表漸進式 rehash 的詳細步驟:
1) 爲 ht[1] 分配空間, 讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。
2) 在字典中維持一個索引計數器變量 rehashidx , 並將它的值設置爲 0 , 表示 rehash 工做正式開始。
3) 在 rehash 進行期間, 每次對字典執行添加、刪除、查找或者更新操做時, 程序除了執行指定的操做之外, 還會順帶將 ht[0] 哈希表在 rehashidx 索引上的全部鍵值對 rehash 到 ht[1] , 當 rehash 工做完成以後, 程序將 rehashidx 屬性的值增一。
4) 隨着字典操做的不斷執行, 最終在某個時間點上, ht[0] 的全部鍵值對都會被 rehash 至 ht[1] , 這時程序將 rehashidx 屬性的值設爲 -1 , 表示 rehash 操做已完成。

漸進式 rehash 的好處在於它採起分而治之的方式, 將 rehash 鍵值對所需的計算工做均灘到對字典的每一個添加、刪除、查找和更新操做上, 從而避免了集中式 rehash 而帶來的龐大計算量。在 rehash 期間,字典的刪改查操做都是同時做用在 ht[0] 以及 ht[1] 上的。如查找一個鍵,會如今 ht[0] 查找,找不到就去 ht[1] 查找,注意的是增長操做,新增的鍵值對只會保存到 ht[1]上,不會保存到 ht[0] 上,這一措施保證了 ht[0] 的鍵值只減不增,隨着 rehash 操做 ht[0] 最終會變成空表。

Redis 的字典實現的特性能夠總結以下:

  • 字典被普遍用於實現 Redis 的各類功能, 其中包括數據庫和哈希鍵。
  • Redis 中的字典使用哈希表做爲底層實現, 每一個字典帶有兩個哈希表, 一個用於平時使用, 另外一個僅在進行 rehash 時使用。
  • 當字典被用做數據庫的底層實現, 或者哈希鍵的底層實現時, Redis 使用 MurmurHash2 算法來計算鍵的哈希值。
  • 哈希表使用鏈地址法來解決鍵衝突, 被分配到同一個索引上的多個鍵值對會鏈接成一個單向鏈表。
  • 在對哈希表進行擴展或者收縮操做時, 程序須要將現有哈希表包含的全部鍵值對 rehash 到新哈希表裏面, 而且這個 rehash 過程並非一次性地完成的, 而是漸進式地完成的。

4、跳躍表

  跳躍表(skiplist)是一種有序數據結構, 它經過在每一個節點中維持多個指向其餘節點的指針, 從而達到快速訪問節點的目的。
跳躍表支持平均 O(\log N) 最壞 O(N) 複雜度的節點查找, 還能夠經過順序性操做來批量處理節點。

  在大部分狀況下, 跳躍表的效率能夠和平衡樹相媲美, 而且由於跳躍表的實現比平衡樹要來得更爲簡單, 因此有很多程序都使用跳躍表來代替平衡樹。

  Redis 使用跳躍表做爲有序集合鍵的底層實現之一: 若是一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員(member)是比較長的字符串時,Redis 就會使用跳躍表來做爲有序集合鍵的底層實現。

  Redis 只在兩個地方用到了跳躍表, 一個是實現有序集合鍵, 另外一個是在集羣節點中用做內部數據結構, 除此以外, 跳躍表在 Redis 裏面沒有其餘用途。

Redis 的跳躍表由 redis.h/zskiplistNode 和 redis.h/zskiplist 兩個結構定義, 其中 zskiplistNode 結構用於表示跳躍表節點, 而 zskiplist 結構則用於保存跳躍表節點的相關信息, 好比節點的數量, 以及指向表頭節點和表尾節點的指針, 等等。

上圖展現了一個跳躍表示例, 位於圖片最左邊的是 zskiplist 結構, 該結構包含如下屬性:

  • header :指向跳躍表的表頭節點。
  • tail :指向跳躍表的表尾節點。
  • level :記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)。
  • length :記錄跳躍表的長度,也便是,跳躍表目前包含節點的數量(表頭節點不計算在內)。

位於 zskiplist 結構右方的是四個 zskiplistNode 結構, 該結構包含如下屬性:

  • 層(level):節點中用 L1 、 L2 、 L3 等字樣標記節點的各個層, L1 表明第一層, L2 表明第二層,以此類推。每一個層都帶有兩個屬性:前進指針和跨度。前進指針用於訪問位於表尾方向的其餘節點,而跨度則記錄了前進指針所指向節點和當前節點的距離。在上面的圖片中,連線上帶有數字的箭頭就表明前進指針,而那個數字就是跨度。當程序從表頭向表尾進行遍歷時,訪問會沿着層的前進指針進行。
  • 後退(backward)指針:節點中用 BW 字樣標記節點的後退指針,它指向位於當前節點的前一個節點。後退指針在程序從表尾向表頭遍歷時使用。
  • 分值(score):各個節點中的 1.0 、 2.0 和 3.0 是節點所保存的分值。在跳躍表中,節點按各自所保存的分值從小到大排列。
  • 成員對象(obj):各個節點中的 o1 、 o2 和 o3 是節點所保存的成員對象。

注意表頭節點和其餘節點的構造是同樣的: 表頭節點也有後退指針、分值和成員對象, 不過表頭節點的這些屬性都不會被用到, 因此圖中省略了這些部分, 只顯示了表頭節點的各個層。

本節接下來的內容將對 zskiplistNode 和 zskiplist 兩個結構進行更詳細的介紹。

  1. 跳躍表節點

跳躍表節點的實現由 redis.h/zskiplistNode 結構定義:

typedef struct zskiplistNode {

    // 後退指針
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成員對象
    robj *obj;

    // 層
    struct zskiplistLevel {

        // 前進指針
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;


跳躍表節點的 level 數組能夠包含多個元素, 每一個元素都包含一個指向其餘節點的指針, 程序能夠經過這些層來加快訪問其餘節點的速度, 通常來講, 層的數量越多, 訪問其餘節點的速度就越快。

每次建立一個新跳躍表節點的時候, 程序都根據冪次定律 (power law,越大的數出現的機率越小) 隨機生成一個介於 1 和 32 之間的值做爲 level 數組的大小, 這個大小就是層的「高度」。

下圖分別展現了三個高度爲 1 層、 3 層和 5 層的節點, 由於 C 語言的數組索引老是從 0 開始的, 因此節點的第一層是 level[0] , 而第二層是 level[1] ,以此類推。

跨度
層的跨度(level[i].span 屬性)用於記錄兩個節點之間的距離:

兩個節點之間的跨度越大, 它們相距得就越遠。
指向 NULL 的全部前進指針的跨度都爲 0 , 由於它們沒有連向任何節點。
初看上去, 很容易覺得跨度和遍歷操做有關, 但實際上並非這樣 —— 遍歷操做只使用前進指針就能夠完成了, 跨度其實是用來計算排位(rank)的: 在查找某個節點的過程當中, 將沿途訪問過的全部層的跨度累計起來, 獲得的結果就是目標節點在跳躍表中的排位。

舉個例子, 下圖用虛線標記了在跳躍表中查找分值爲 3.0 、 成員對象爲 o3 的節點時, 沿途經歷的層: 查找的過程只通過了一個層, 而且層的跨度爲 3 , 因此目標節點在跳躍表中的排位爲 3 。

後退指針
節點的後退指針(backward 屬性)用於從表尾向表頭方向訪問節點: 跟能夠一次跳過多個節點的前進指針不一樣, 由於每一個節點只有一個後退指針, 因此每次只能後退至前一個節點。

下圖用虛線展現了若是從表尾向表頭遍歷跳躍表中的全部節點: 程序首先經過跳躍表的 tail 指針訪問表尾節點, 而後經過後退指針訪問倒數第二個節點, 以後再沿着後退指針訪問倒數第三個節點, 再以後遇到指向 NULL 的後退指針, 因而訪問結束。

分值和成員

節點的分值(score 屬性)是一個 double 類型的浮點數, 跳躍表中的全部節點都按分值從小到大來排序。

節點的成員對象(obj 屬性)是一個指針, 它指向一個字符串對象, 而字符串對象則保存着一個 SDS 值。

在同一個跳躍表中, 各個節點保存的成員對象必須是惟一的, 可是多個節點保存的分值卻能夠是相同的: 分值相同的節點將按照成員對象在字典序中的大小來進行排序, 成員對象較小的節點會排在前面(靠近表頭的方向), 而成員對象較大的節點則會排在後面(靠近表尾的方向)。

舉個例子, 在下圖所示的跳躍表中, 三個跳躍表節點都保存了相同的分值 10086.0 , 但保存成員對象 o1 的節點卻排在保存成員對象 o2 和 o3 的節點以前, 而保存成員對象 o2 的節點又排在保存成員對象 o3 的節點以前, 因而可知, o1 、 o2 、 o3 三個成員對象在字典中的排序爲 o1 <= o2 <= o3 。

  1. 跳躍表
    雖然僅靠多個跳躍表節點就能夠組成一個跳躍表, 以下圖所示。

    但經過使用一個 zskiplist 結構來持有這些節點, 程序能夠更方便地對整個跳躍表進行處理, 好比快速訪問跳躍表的表頭節點和表尾節點, 又或者快速地獲取跳躍表節點的數量(也便是跳躍表的長度)等信息, 以下圖所示。

    zskiplist 結構的定義以下:
typedef struct zskiplist {

    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;

    // 表中節點的數量
    unsigned long length;

    // 表中層數最大的節點的層數
    int level;

} zskiplist;

header 和 tail 指針分別指向跳躍表的表頭和表尾節點, 經過這兩個指針, 程序定位表頭節點和表尾節點的複雜度爲 O(1) 。

經過使用 length 屬性來記錄節點的數量, 程序能夠在 O(1) 複雜度內返回跳躍表的長度。

level 屬性則用於在 O(1) 複雜度內獲取跳躍表中層高最大的那個節點的層數量, 注意表頭節點的層高並不計算在內。

關於跳躍表的總結:

  • 跳躍表是有序集合的底層實現之一, 除此以外它在 Redis 中沒有其餘應用。
  • Redis 的跳躍表實現由 zskiplist 和 zskiplistNode 兩個結構組成, 其中 zskiplist 用於保存跳躍表信息(好比表頭節點、表尾節點、長度), 而 zskiplistNode 則用於表示跳躍表節點。
  • 每一個跳躍表節點的層高都是 1 至 32 之間的隨機數。
  • 在同一個跳躍表中, 多個節點能夠包含相同的分值, 但每一個節點的成員對象必須是惟一的。
  • 跳躍表中的節點按照分值大小進行排序, 當分值相同時, 節點按照成員對象的大小進行排序。

5、整數集合

整數集合(intset)是集合鍵的底層實現之一: 當一個集合只包含整數值元素, 而且這個集合的元素數量很少時, Redis 就會使用整數集合做爲集合鍵的底層實現。

舉個例子, 若是咱們建立一個只包含五個元素的集合鍵, 而且集合中的全部元素都是整數值, 那麼這個集合鍵的底層實現就會是整數集合:

redis> SADD numbers 1 3 5 7 9
(integer) 5

redis> OBJECT ENCODING numbers
"intset"

整數集合(intset)是 Redis 用於保存整數值的集合抽象數據結構, 它能夠保存類型爲 int16_t 、 int32_t 或者 int64_t 的整數值, 而且保證集合中不會出現重複元素。

每一個 intset.h/intset 結構表示一個整數集合:

typedef struct intset {

    // 編碼方式
    uint32_t encoding;

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

    // 保存元素的數組
    int8_t contents[];

} intset;

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

length 屬性記錄了整數集合包含的元素數量, 也便是 contents 數組的長度。

雖然 intset 結構將 contents 屬性聲明爲 int8_t 類型的數組, 但實際上 contents 數組並不保存任何 int8_t 類型的值 —— contents 數組的真正類型取決於 encoding 屬性的值:若是 encoding 屬性的值爲 INTSET_ENC_INT16 , 那麼 contents 就是一個 int16_t 類型的數組, 數組裏的每一個項都是一個 int16_t 類型的整數值 (最小值爲 -32,768 ,最大值爲 32,767 )。
下圖是一個包含五個int16_t類型整數值的整數集合。

每當咱們要將一個新元素添加到整數集合裏面, 而且新元素的類型比整數集合現有全部元素的類型都要長時, 整數集合須要先進行升級(upgrade), 而後才能將新元素添加到整數集合裏面。

升級整數集合並添加新元素共分爲三步進行:

  • 根據新元素的類型, 擴展整數集合底層數組的空間大小, 併爲新元素分配空間。
  • 將底層數組現有的全部元素都轉換成與新元素相同的類型, 並將類型轉換後的元素放置到正確的位上, 並且在放置元素的過程當中, 須要繼續維持底層數組的有序性質不變。
  • 將新元素添加到底層數組裏面。

整數集合不支持降級操做, 一旦對數組進行了升級, 編碼就會一直保持升級後的狀態。

關於整數集合的總結:

  • 整數集合是集合鍵的底層實現之一。
  • 整數集合的底層實現爲數組, 這個數組以有序、無重複的方式保存集合元素, 在有須要時, 程序會根據新添加元素的類型, 改變這個數組的類型。
  • 升級操做爲整數集合帶來了操做上的靈活性, 而且儘量地節約了內存。
  • 整數集合只支持升級操做, 不支持降級操做。

6、壓縮列表

壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。

當一個列表鍵只包含少許列表項, 而且每一個列表項要麼就是小整數值, 要麼就是長度比較短的字符串, 那麼 Redis 就會使用壓縮列表來作列表鍵的底層實現。

好比說, 執行如下命令將建立一個壓縮列表實現的列表鍵:

redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6

redis> OBJECT ENCODING lst
"ziplist"

由於列表鍵裏面包含的都是 1 、 3 、 5 、 10086 這樣的小整數值, 以及 "hello" 、 "world" 這樣的短字符串。

另外, 當一個哈希鍵只包含少許鍵值對, 而且每一個鍵值對的鍵和值要麼就是小整數值, 要麼就是長度比較短的字符串, 那麼 Redis 就會使用壓縮列表來作哈希鍵的底層實現。

舉個例子, 執行如下命令將建立一個壓縮列表實現的哈希鍵:

redis> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK

redis> OBJECT ENCODING profile
"ziplist"

由於哈希鍵裏面包含的全部鍵和值都是小整數值或者短字符串。
壓縮列表的構成:
壓縮列表是 Redis 爲了節約內存而開發的, 由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。

一個壓縮列表能夠包含任意多個節點(entry), 每一個節點能夠保存一個字節數組或者一個整數值。

下圖展現了壓縮列表的各個組成部分。

下表則記錄了各個組成部分的類型、長度、以及用途。表 7-1 則記錄了各個組成部分的類型、長度、以及用途。

屬性 類型 長度 用途
zlbytes uint32_t 4 字節 記錄整個壓縮列表佔用的內存字節數:在對壓縮列表進行內存重分配, 或者計算 zlend 的位置時使用。
zltail uint32_t 4 字節 記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少字節: 經過這個偏移量,程序無須遍歷整個壓縮列表就能夠肯定表尾節點的地址。
zllen uint16_t 2 字節 記錄了壓縮列表包含的節點數量: 當這個屬性的值小於 UINT16_MAX (65535)時, 這個屬性的值就是壓縮列表包含節點的數量; 當這個值等於 UINT16_MAX 時, 節點的真實數量須要遍歷整個壓縮列表才能計算得出。
entryX 列表節點 不定 壓縮列表包含的各個節點,節點的長度由節點保存的內容決定。
zlend uint8_t 1 字節 特殊值 0xFF (十進制 255 ),用於標記壓縮列表的末端。

壓縮列表節點的構成:
每一個壓縮列表節點都由 previous_entry_length 、 encoding 、 content 三個部分組成, 以下圖所示。

  • 節點的 previous_entry_length 屬性以字節爲單位, 記錄了壓縮列表中前一個節點的長度。
  • 節點的 encoding 屬性記錄了節點的 content 屬性所保存數據的類型以及長度:
  • 節點的 content 屬性負責保存節點的值, 節點值能夠是一個字節數組或者整數, 值的類型和長度由節點的 encoding 屬性決定。
    關於壓縮列表的總結:
  • 壓縮列表是一種爲節約內存而開發的順序型數據結構。
  • 壓縮列表被用做列表鍵和哈希鍵的底層實現之一。
  • 壓縮列表能夠包含多個節點,每一個節點能夠保存一個字節數組或者整數值。
  • 添加新節點到壓縮列表, 或者從壓縮列表中刪除節點, 可能會引起連鎖更新操做, 但這種操做出現的概率並不高。
  • 最後

若是以爲還不錯請點贊關注轉發,不勝感激。

更多精彩內容 微信搜素: 蘑菇睡不着

相關文章
相關標籤/搜索