Redis五大類型及底層實現原理

目錄

簡單動態字符串 
鏈表 
字典 
跳躍表 
整數集合 
壓縮列表 
對象 node

對象的類型與編碼
字符串對象
列表對象
哈希對象redis

集合對象
有序集合對象
類型檢查與命令多態
內存回收
對象共享
對象的空轉時長算法

 

簡單動態字符串 

導讀

  • Redis 只會使用 C 字符串做爲字面量, 在大多數狀況下, Redis 使用 SDS (Simple Dynamic String,簡單動態字符串)做爲字符串表示。
  • 比起 C 字符串, SDS 具備如下優勢:
    1. 常數複雜度獲取字符串長度。
    2. 杜絕緩衝區溢出。
    3. 減小修改字符串長度時所需的內存重分配次數。
    4. 二進制安全。
    5. 兼容部分 C 字符串函數。

 

簡單動態字符串

Redis 沒有直接使用 C 語言傳統的字符串表示(以空字符結尾的字符數組,如下簡稱 C 字符串), 而是本身構建了一種名爲簡單動態字符串(simple dynamic string,SDS)的抽象類型, 並將 SDS 用做 Redis 的默認字符串表示。數據庫

SDS 的定義

每一個 sds.h/sdshdr 結構表示一個 SDS 值:數組

struct sdshdr {

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

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

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

};

 

SDS vs C字符串

表 2-1 C 字符串和 SDS 之間的區別緩存

C 字符串 SDS
獲取字符串長度的複雜度爲O(N)。 獲取字符串長度的複雜度爲O(1)。
API 是不安全的,可能會形成緩衝區溢出。 API 是安全的,不會形成緩衝區溢出。
修改字符串長度N次必然須要執行N次內存重分配。 修改字符串長度N次最多須要執行N次內存重分配。
只能保存文本數據。 能夠保存文本或者二進制數據。
可使用全部<string.h>庫中的函數。 可使用一部分<string.h>庫中的函數。

 

常數複雜度獲取字符串長度

經過使用 SDS 而不是 C 字符串, Redis 將獲取字符串長度所需的複雜度從 O(N) 下降到了 O(1) , 這確保了獲取字符串長度的工做不會成爲 Redis 的性能瓶頸。安全

 

杜絕緩衝區溢出

 減小修改字符串時帶來的內存重分配次數:經過未使用空間, SDS 實現了空間預分配和惰性空間釋放兩種優化策略。服務器

  1. 空間預分配 - 經過這種策略, SDS 將連續增加 N 次字符串所需的內存重分配次數從一定 N 次下降爲最多 N 次。

空間預分配用於優化 SDS 的字符串增加操做: 當 SDS 的 API 對一個 SDS 進行修改, 而且須要對 SDS 進行空間擴展的時候, 程序不只會爲 SDS 分配修改所必需要的空間, 還會爲 SDS 分配額外的未使用空間。數據結構

其中, 額外分配的未使用空間數量由如下公式決定:app

  • 若是對 SDS 進行修改以後, SDS 的長度(也便是 len 屬性的值)將小於 1 MB , 那麼程序分配和 len 屬性一樣大小的未使用空間, 這時 SDS len 屬性的值將和 free 屬性的值相同。 舉個例子, 若是進行修改以後, SDS 的 len 將變成 13 字節, 那麼程序也會分配 13 字節的未使用空間, SDS 的 buf 數組的實際長度將變成 13 + 13 + 1 = 27 字節(額外的一字節用於保存空字符)。
  • 若是對 SDS 進行修改以後, SDS 的長度將大於等於 1 MB , 那麼程序會分配 1 MB 的未使用空間。 舉個例子, 若是進行修改以後, SDS 的 len 將變成 30 MB , 那麼程序會分配 1 MB 的未使用空間, SDS 的 buf 數組的實際長度將爲 30 MB + 1 MB + 1 byte 。
  1. 惰性空間釋放 - 經過這種策略, SDS 避免了縮短字符串時所需的內存重分配操做, 併爲未來可能有的增加操做提供了優化。

惰性空間釋放用於優化 SDS 的字符串縮短操做: 當 SDS 的 API 須要縮短 SDS 保存的字符串時, 程序並不當即使用內存重分配來回收縮短後多出來的字節, 而是使用 free 屬性將這些字節的數量記錄起來, 並等待未來使用。

與此同時, SDS 也提供了相應的 API , 讓咱們能夠在有須要時, 真正地釋放 SDS 裏面的未使用空間, 因此不用擔憂惰性空間釋放策略會形成內存浪費。 

 

二進制安全

  • 全部 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裏的數據, 程序不會對其中的數據作任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。這也是咱們將 SDS 的 buf 屬性稱爲字節數組的緣由 —— Redis 不是用這個數組來保存字符, 而是用它來保存一系列二進制數據。
  • SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結束。
  • 經過使用二進制安全的 SDS , 而不是 C 字符串, 使得 Redis 不只能夠保存文本數據, 還能夠保存任意格式的二進制數據。

 

兼容部分 C 字符串函數

雖然 SDS 的 API 都是二進制安全的, 但它們同樣遵循 C 字符串以空字符結尾的慣例: 這些 API 總會將 SDS 保存的數據的末尾設置爲空字符, 而且總會在爲 buf 數組分配空間時多分配一個字節來容納這個空字符, 這是爲了讓那些保存文本數據的 SDS 能夠重用一部分 <string.h> 庫定義的函數。這樣 Redis 就不用本身專門去實現一套函數。

表 2-2 SDS 的主要操做 API

函數 做用 時間複雜度
sdsnew 建立一個包含給定 C 字符串的 SDS 。 O(N),N爲給定 C 字符串的長度。
sdsempty 建立一個不包含任何內容的空 SDS 。 O(1)
sdsfree 釋放給定的 SDS 。 O(1)
sdslen 返回 SDS 的已使用空間字節數。 這個值能夠經過讀取 SDS 的len屬性來直接得到, 複雜度爲O(1)。
sdsavail 返回 SDS 的未使用空間字節數。

這個值能夠經過讀取 SDS 的free屬性來直接得到, 複雜度爲

O(1)。

sdsdup 建立一個給定 SDS 的副本(copy)。 O(N),N爲給定 SDS 的長度。
sdsclear 清空 SDS 保存的字符串內容。 由於惰性空間釋放策略,複雜度爲O(1)。
sdscat 將給定 C 字符串拼接到 SDS 字符串的末尾。 O(N),N爲被拼接 C 字符串的長度。
sdscatsds 將給定 SDS 字符串拼接到另外一個 SDS 字符串的末尾。 O(N),N爲被拼接 SDS 字符串的長度。
sdscpy 將給定的 C 字符串複製到 SDS 裏面, 覆蓋 SDS 原有的字符串。 O(N),N爲被複制 C 字符串的長度。
sdsgrowzero 用空字符將 SDS 擴展至給定長度。 O(N),N爲擴展新增的字節數。
sdsrange 保留 SDS 給定區間內的數據, 不在區間內的數據會被覆蓋或清除。 O(N),N爲被保留數據的字節數。
sdstrim 接受一個 SDS 和一個 C 字符串做爲參數, 從 SDS 左右兩端分別移除全部在 C 字符串中出現過的字符。 O(M*N),M爲 SDS 的長度,N爲給定 C 字符串的長度。
sdscmp 對比兩個 SDS 字符串是否相同。 O(N),N爲兩個 SDS 中較短的那個 SDS 的長度。

 

 


 

鏈表

導讀

鏈表提供了高效的節點重排能力, 以及順序性的節點訪問方式, 而且能夠經過增刪節點來靈活地調整鏈表的長度。由於 Redis 使用的 C 語言並無內置這種數據結構, 因此 Redis 構建了本身的鏈表實現。

  • 鏈表被普遍用於實現 Redis 的各類功能, 好比列表鍵, 發佈與訂閱, 慢查詢, 監視器, 等等。
  • 每一個鏈表節點由一個 listNode 結構來表示, 每一個節點都有一個指向前置節點和後置節點的指針, 因此 Redis 的鏈表實現是雙端鏈表。
  • 每一個鏈表使用一個 list 結構來表示, 這個結構帶有表頭節點指針、表尾節點指針、以及鏈表長度等信息。
  • 由於鏈表表頭節點的前置節點和表尾節點的後置節點都指向 NULL , 因此 Redis 的鏈表實現是無環鏈表。
  • 經過爲鏈表設置不一樣的類型特定函數, Redis 的鏈表能夠用於保存各類不一樣類型的值。

 

鏈表和鏈表節點的實現

每一個鏈表節點使用一個 adlist.h/listNode 結構來表示:

1 typedef struct listNode {
 2 
 3     // 前置節點
 4     struct listNode *prev;
 5 
 6     // 後置節點
 7     struct listNode *next;
 8 
 9     // 節點的值
10     void *value;
11 
12 } listNode;

 

多個 listNode 能夠經過 prev 和 next 指針組成雙端鏈表, 如圖 3-1 所示。

 

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

 1 typedef struct list {
 2 
 3     // 表頭節點
 4     listNode *head;
 5 
 6     // 表尾節點
 7     listNode *tail;
 8 
 9     // 鏈表所包含的節點數量
10     unsigned long len;
11 
12     // 節點值複製函數
13     void *(*dup)(void *ptr);
14 
15     // 節點值釋放函數
16     void (*free)(void *ptr);
17 
18     // 節點值對比函數
19     int (*match)(void *ptr, void *key);
20 
21 } list;

 

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

  • dup 函數用於複製鏈表節點所保存的值;
  • free 函數用於釋放鏈表節點所保存的值;
  • match 函數則用於對比鏈表節點所保存的值和另外一個輸入值是否相等。

圖 3-2 是由一個 list 結構和三個 listNode 結構組成的鏈表:

 

 

 

 

鏈表和鏈表節點的 API

函數 做用 時間複雜度
listSetDupMethod 將給定的函數設置爲鏈表的節點值複製函數。 O(1)。
listGetDupMethod 返回鏈表當前正在使用的節點值複製函數。

複製函數能夠經過鏈表的dup屬性直接得到,

O(1)

listSetFreeMethod 將給定的函數設置爲鏈表的節點值釋放函數。 O(1)。
listGetFree 返回鏈表當前正在使用的節點值釋放函數。

釋放函數能夠經過鏈表的free屬性直接得到,

O(1)

listSetMatchMethod 將給定的函數設置爲鏈表的節點值對比函數。 O(1)
listGetMatchMethod 返回鏈表當前正在使用的節點值對比函數。

對比函數能夠經過鏈表的match

屬性直接得到,

O(1)

listLength 返回鏈表的長度(包含了多少個節點)。

鏈表長度能夠經過鏈表的len屬性直接得到,

O(1)

listFirst 返回鏈表的表頭節點。

表頭節點能夠經過鏈表的head屬性直接得到,

O(1)

listLast 返回鏈表的表尾節點。

表尾節點能夠經過鏈表的tail屬性直接得到,

O(1)

listPrevNode 返回給定節點的前置節點。

前置節點能夠經過節點的prev屬性直接得到,

O(1)

listNextNode 返回給定節點的後置節點。

後置節點能夠經過節點的next屬性直接得到,

O(1)

listNodeValue 返回給定節點目前正在保存的值。

節點值能夠經過節點的value屬性直接得到,

O(1)

listCreate 建立一個不包含任何節點的新鏈表。 O(1)
listAddNodeHead 將一個包含給定值的新節點添加到給定鏈表的表頭。 O(1)
listAddNodeTail 將一個包含給定值的新節點添加到給定鏈表的表尾。 O(1)
listInsertNode 將一個包含給定值的新節點添加到給定節點的以前或者以後。 O(1)
listSearchKey 查找並返回鏈表中包含給定值的節點。 O(N),N爲鏈表長度。
listIndex 返回鏈表在給定索引上的節點。 O(N),N爲鏈表長度。
listDelNode 從鏈表中刪除給定節點。 O(1)
listRotate 將鏈表的表尾節點彈出,而後將被彈出的節點插入到鏈表的表頭, 成爲新的表頭節點。 O(1)
listDup 複製一個給定鏈表的副本。 O(N),N爲鏈表長度。
listRelease 釋放給定鏈表,以及鏈表中的全部節點。 O(N),N爲鏈表長度。

 


 

字典

Redis 所使用的 C 語言並無內置這種數據結構, 所以 Redis 構建了本身的字典實現。

  • 字典, 又稱符號表(symbol table)、關聯數組(associative array)或者映射(map), 是一種用於保存鍵值對(key-value pair)的抽象數據結構。
  • 在字典中, 一個鍵(key)能夠和一個值(value)進行關聯(或者說將鍵映射爲值), 這些關聯的鍵和值就被稱爲鍵值對。
  • 字典中的每一個鍵都是獨一無二的, 程序能夠在字典中根據鍵查找與之關聯的值, 或者經過鍵來更新值, 又或者根據鍵來刪除整個鍵值對, 等等。

 

導讀

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

 

字典的實現

Redis 的字典使用哈希表做爲底層實現, 一個哈希表裏面能夠有多個哈希表節點, 而每一個哈希表節點就保存了字典中的一個鍵值對

 

字典

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

 1 typedef struct dict {
 2 
 3     // 類型特定函數
 4     dictType *type;
 5 
 6     // 私有數據
 7     void *privdata;
 8 
 9     // 哈希表
10     dictht ht[2];
11 
12     // rehash 索引
13     // 當 rehash 不在進行時,值爲 -1
14     int rehashidx; /* rehashing not in progress if rehashidx == -1 */
15 
16 } dict;

 

  • type 屬性和 privdata 屬性是針對不一樣類型的鍵值對, 爲建立多態字典而設置的:
  • type 屬性是一個指向 dictType 結構的指針, 每一個 dictType 結構保存了一簇用於操做特定類型鍵值對的函數, Redis 會爲用途不一樣的字典設置不一樣的類型特定函數。
  • 而 privdata 屬性則保存了須要傳給那些類型特定函數的可選參數。
  • ht 屬性是一個包含兩個項的數組, 數組中的每一個項都是一個 dictht 哈希表, 通常狀況下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只會在對 ht[0] 哈希表進行 rehash 時使用。
  • 除了 ht[1] 以外, 另外一個和 rehash 有關的屬性就是 rehashidx : 它記錄了 rehash 目前的進度, 若是目前沒有在進行 rehash , 那麼它的值爲 -1 。

 

哈希表

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

 1 typedef struct dictht {
 2 
 3     // 哈希表數組
 4     dictEntry **table;
 5 
 6     // 哈希表大小
 7     unsigned long size;
 8 
 9     // 哈希表大小掩碼,用於計算索引值
10     // 老是等於 size - 1
11     unsigned long sizemask;
12 
13     // 該哈希表已有節點的數量
14     unsigned long used;
15 
16 } dictht;
17 table 屬性是一個數組, 數組中的每一個元素都是一個指向 dict.h/dictEntry 結構的指針, 每一個 dictEntry 結構保存着一個鍵值對。

 

  • table 屬性是一個數組, 數組中的每一個元素都是一個指向 dict.h/dictEntry 結構的指針, 每一個 dictEntry 結構保存着一個鍵值對。
  • size 屬性記錄了哈希表的大小, 也便是 table 數組的大小, 而 used 屬性則記錄了哈希表目前已有節點(鍵值對)的數量。
  • sizemask 屬性的值老是等於 size - 1 , 這個屬性和哈希值一塊兒決定一個鍵應該被放到 table 數組的哪一個索引上面。

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

 

 

 

 

哈希表節點

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

 1 typedef struct dictEntry {
 2 
 3     //
 4     void *key;
 5 
 6     //
 7     union {
 8         void *val;
 9         uint64_t u64;
10         int64_t s64;
11     } v;
12 
13     // 指向下個哈希表節點,造成鏈表
14     struct dictEntry *next;
15 
16 } dictEntry;

 

  • key 屬性保存着鍵值對中的鍵。
  • v 屬性則保存着鍵值對中的值, 其中鍵值對的值能夠是一個指針, 或者是一個 uint64_t 整數, 又或者是一個 int64_t 整數。
  • next 屬性是指向另外一個哈希表節點的指針, 這個指針能夠將多個哈希值相同的鍵值對鏈接在一次, 以此來解決鍵衝突(collision)的問題。

 

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

 

 

 

 

哈希算法

當要將一個新的鍵值對添加到字典裏面時, 程序須要先根據鍵值對的鍵計算出哈希值和索引值, 而後再根據索引值, 將包含新鍵值對的哈希表節點放到哈希表數組的指定索引上面。

Redis 計算哈希值和索引值的方法以下:

1 # 使用字典設置的哈希函數,計算鍵 key 的哈希值
2 hash = dict->type->hashFunction(key);
3 
4 # 使用哈希表的 sizemask 屬性和哈希值,計算出索引值
5 # 根據狀況不一樣, ht[x] 能夠是 ht[0] 或者 ht[1]
6 index = hash & dict->ht[x].sizemask;

 

 

 

舉個例子, 對於圖 4-4 所示的字典來講, 若是咱們要將一個鍵值對 k0 和 v0 添加到字典裏面, 那麼程序會先使用語句:

hash = dict->type->hashFunction(k0);

 

計算鍵 k0 的哈希值。

假設計算得出的哈希值爲 8 , 那麼程序會繼續使用語句:

index = hash & dict->ht[0].sizemask = 8 & 3 = 0;

計算出鍵 k0 的索引值 0 , 這表示包含鍵值對 k0 和 v0 的節點應該被放置到哈希表數組的索引 0 位置上, 如圖 4-5 所示。

 

 

 

當字典被用做數據庫的底層實現, 或者哈希鍵的底層實現時, Redis 使用 MurmurHash2 算法來計算鍵的哈希值。

MurmurHash 算法最初由 Austin Appleby 於 2008 年發明, 這種算法的優勢在於, 即便輸入的鍵是有規律的, 算法仍能給出一個很好的隨機分佈性, 而且算法的計算速度也很是快。

MurmurHash 算法目前的最新版本爲 MurmurHash3 , 而 Redis 使用的是 MurmurHash2 , 關於 MurmurHash 算法的更多信息能夠參考該算法的主頁: http://code.google.com/p/smhasher/ 。

 

解決鍵衝突

哈希表節點的next 屬性是用來解決鍵衝突(collision)的問題,它指向另外一個哈希表節點的指針。

  • 當有兩個或以上數量的鍵被分配到了哈希表數組的同一個索引上面時, 咱們稱這些鍵發生了衝突(collision)。
  • Redis 的哈希表使用鏈地址法(separate chaining)來解決鍵衝突: 每一個哈希表節點都有一個 next 指針, 多個哈希表節點能夠用 next 指針構成一個單向鏈表, 被分配到同一個索引上的多個節點能夠用這個單向鏈表鏈接起來, 這就解決了鍵衝突的問題。

由於 dictEntry 節點組成的鏈表沒有指向鏈表表尾的指針, 因此爲了速度考慮, 程序老是將新節點添加到鏈表的表頭位置(複雜度爲 O(1)), 排在其餘已有節點的前面。

 

rehash

隨着操做的不斷執行, 哈希表保存的鍵值對會逐漸地增多或者減小, 爲了讓哈希表的負載因子(load factor)維持在一個合理的範圍以內, 當哈希表保存的鍵值對數量太多或者太少時, 程序須要對哈希表的大小進行相應的擴展或者收縮。

擴展和收縮哈希表的工做能夠經過執行 rehash (從新散列)操做來完成, Redis 對字典的哈希表執行 rehash 的步驟以下:

  1. 爲字典的 ht[1] 哈希表分配空間, 這個哈希表的空間大小取決於要執行的操做, 以及 ht[0] 當前包含的鍵值對數量 (也便是 ht[0].used 屬性的值):
       若是執行的是擴展操做, 那麼 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 作準備。

舉個例子, 假設程序要對含有5個鍵值對字典的 ht[0] 進行擴展操做, 那麼程序將執行如下步驟:

  1. ht[0].used 當前的值爲 5 , 5 * 2 = 10 , 而 第一個大於等於10的且2 的 n 次方的數是16, 因此程序會將 ht[1] 哈希表的大小設置爲 16 。
  2. 將 ht[0] 包含的5個鍵值對都 rehash 到 ht[1]。
  3. 釋放 ht[0] ,並將 ht[1] 設置爲 ht[0] ,而後爲 ht[1] 分配一個空白哈希表。

 

哈希表的擴展與收縮

  1. 當哈希表的負載因子小於 0.1 時, 程序自動開始對哈希表執行收縮操做。
  2. 當如下條件中的任意一個被知足時, 程序會自動開始對哈希表執行擴展操做:
  • 服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於 1 ;
  • 服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於 5 ;

其中哈希表的負載因子能夠經過公式計算得出:

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

 

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

 

漸進式 rehash

  • 上一節說過, 擴展或收縮哈希表須要將 ht[0] 裏面的全部鍵值對 rehash 到 ht[1] 裏面, 可是, 這個 rehash 動做並非一次性、集中式地完成的, 而是分屢次、漸進式地完成的。
  • 緣由在於, 若是哈希表裏保存的鍵值對數量巨大, 有四百萬、四千萬甚至四億個鍵值對, 那麼要一次性將這些鍵值對所有 rehash 到 ht[1] 的話, 龐大的計算量可能會致使服務器在一段時間內中止服務。
  • 所以, 爲了不 rehash 對服務器性能形成影響, 服務器不是一次性將 ht[0] 裏面的全部鍵值對所有 rehash 到 ht[1] , 而是分屢次、漸進式地將 ht[0] 裏面的鍵值對慢慢地 rehash 到 ht[1] 。
  • 漸進式 rehash 的好處在於它採起分而治之的方式, 將 rehash 鍵值對所需的計算工做均灘到對字典的每一個添加、刪除、查找和更新操做上, 從而避免了集中式 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完,又須要擴容狀況怎麼辦?

 

字典 API

表 4-1 字典的主要操做 API

函數 做用 時間複雜度
dictCreate 建立一個新的字典。 O(1)
dictAdd 將給定的鍵值對添加到字典裏面。 O(1)
dictReplace 將給定的鍵值對添加到字典裏面, 若是鍵已經存在於字典,那麼用新值取代原有的值。 O(1)
dictFetchValue 返回給定鍵的值。 O(1)
dictGetRandomKey 從字典中隨機返回一個鍵值對。 O(1)
dictDelete 從字典中刪除給定鍵所對應的鍵值對。 O(1)
dictRelease 釋放給定字典,以及字典中包含的全部鍵值對。 O(N),N爲字典包含的鍵值對數量。

 


 

 

跳躍表

導讀

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

 

跳躍表的實現

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

 

 

 圖 5-1 展現了一個跳躍表示例, 位於圖片最左邊的是 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

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

 1 typedef struct zskiplistNode {
 2 
 3     // 後退指針
 4     struct zskiplistNode *backward;
 5 
 6     // 分值
 7     double score;
 8 
 9     // 成員對象
10     robj *obj;
11 
12     //
13     struct zskiplistLevel {
14 
15         // 前進指針
16         struct zskiplistNode *forward;
17 
18         // 跨度
19         unsigned int span;
20 
21     } level[];
22 
23 } zskiplistNode;

 

 

  • 跳躍表節點的 level 數組能夠包含多個元素, 每一個元素都包含一個指向其餘節點的指針, 程序能夠經過這些層來加快訪問其餘節點的速度, 通常來講, 層的數量越多, 訪問其餘節點的速度就越快。
  • 每次建立一個新跳躍表節點的時候, 程序都根據冪次定律 (power law,越大的數出現的機率越小) 隨機生成一個介於 1 和 32 之間的值做爲 level 數組的大小, 這個大小就是層的「高度」。

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

 

 

 

 

前進指針

每一個層都有一個指向表尾方向的前進指針(level[i].forward 屬性), 用於從表頭向表尾方向訪問節點。

圖 5-3 用虛線表示出了程序從表頭向表尾方向, 遍歷跳躍表中全部節點的路徑:

  1. 迭代程序首先訪問跳躍表的第一個節點(表頭), 而後從第四層的前進指針移動到表中的第二個節點。
  2. 在第二個節點時, 程序沿着第二層的前進指針移動到表中的第三個節點。
  3. 在第三個節點時, 程序一樣沿着第二層的前進指針移動到表中的第四個節點。
  4. 當程序再次沿着第四個節點的前進指針移動時, 它碰到一個 NULL , 程序知道這時已經到達了跳躍表的表尾, 因而結束此次遍歷。

 

 

跨度

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

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

再舉個例子, 圖 5-5 用虛線標記了在跳躍表中查找分值爲 2.0 、 成員對象爲 o2 的節點時, 沿途經歷的層: 在查找節點的過程當中, 程序通過了兩個跨度爲 1 的節點, 所以能夠計算出, 目標節點在跳躍表中的排位爲 2 。

 

  

後退指針

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

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

 

 

 

分值和成員

  • 節點的分值(score 屬性)是一個 double 類型的浮點數, 跳躍表中的全部節點都按分值從小到大來排序
  • 節點的成員對象(obj 屬性)是一個指針, 它指向一個字符串對象, 而字符串對象則保存着一個 SDS 值。
  • 在同一個跳躍表中, 各個節點保存的成員對象必須是惟一的, 可是多個節點保存的分值卻能夠是相同的: 分值相同的節點將按照成員對象在字典序中的大小來進行排序, 成員對象較小的節點會排在前面(靠近表頭的方向), 而成員對象較大的節點則會排在後面(靠近表尾的方向)。

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

 

 

 

跳躍表 zskiplist

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

zskiplist 結構的定義以下:

 1 typedef struct zskiplist {
 2 
 3     // 表頭節點和表尾節點
 4     struct zskiplistNode *header, *tail;
 5 
 6     // 表中節點的數量
 7     unsigned long length;
 8 
 9     // 表中層數最大的節點的層數
10     int level;
11 
12 } zskiplist;
13 header 和 tail 指針分別指向跳躍表的表頭和表尾節點, 經過這兩個指針, 程序定位表頭節點和表尾節點的複雜度爲 O(1) 。

 

  • header 和 tail 指針分別指向跳躍表的表頭和表尾節點, 經過這兩個指針, 程序定位表頭節點和表尾節點的複雜度爲 O(1) 。
  • length 屬性用來記錄節點的數量, 程序能夠在 O(1) 複雜度內返回跳躍表的長度。
  • level 屬性則用於在 O(1) 複雜度內獲取跳躍表中層高最大的那個節點的層數量, 注意表頭節點的層高並不計算在

 

 

 

 


 

 

整數集合

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

 

導讀

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

 

整數集合的實現

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

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

1 typedef struct intset {
 2 
 3     // 編碼方式
 4     uint32_t encoding;
 5 
 6     // 集合包含的元素數量
 7     uint32_t length;
 8 
 9     // 保存元素的數組
10     int8_t contents[];
11 
12 } intset;
13 contents 數組是整數集合的底層實現: 整數集合的每一個元素都是 contents 數組的一個數組項(item), 各個項在數組中按值的大小從小到大有序地排列, 而且數組中不包含任何重複項。

 

  • 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 )。
  • 若是 encoding 屬性的值爲 INTSET_ENC_INT32 , 那麼 contents 就是一個 int32_t 類型的數組, 數組裏的每一個項都是一個 int32_t 類型的整數值 (最小值爲 -2,147,483,648 ,最大值爲 2,147,483,647 )。
  • 若是 encoding 屬性的值爲 INTSET_ENC_INT64 , 那麼 contents 就是一個 int64_t 類型的數組, 數組裏的每一個項都是一個 int64_t 類型的整數值 (最小值爲 -9,223,372,036,854,775,808 ,最大值爲 9,223,372,036,854,775,807 )。

 

升級

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

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

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

由於每次向整數集合添加新元素均可能會引發升級, 而每次升級都須要對底層數組中已有的全部元素進行類型轉換, 因此向整數集合添加新元素的時間複雜度爲 O(N) 。

 

升級以後新元素的擺放位置

由於引起升級的新元素的長度老是比整數集合現有全部元素的長度都大, 因此這個新元素的值要麼就大於全部現有元素, 要麼就小於全部現有元素:

  • 在新元素小於全部現有元素的狀況下, 新元素會被放置在底層數組的最開頭(索引 0 );
  • 在新元素大於全部現有元素的狀況下, 新元素會被放置在底層數組的最末尾(索引 length-1 )。

 

升級的好處

整數集合的升級策略有兩個好處, 一個是提高整數集合的靈活性, 另外一個是儘量地節約內存。

 

提高靈活性

  • 由於 C 語言是靜態類型語言, 爲了不類型錯誤, 咱們一般不會將兩種不一樣類型的值放在同一個數據結構裏面。
  • 整數集合能夠經過自動升級底層數組來適應新元素, 因此咱們能夠隨意地將 int16_t 、 int32_t 或者 int64_t 類型的整數添加到集合中, 而沒必要擔憂出現類型錯誤, 這種作法很是靈活。

 

節約內存

  • 要讓一個數組能夠同時保存 int16_t 、 int32_t 、 int64_t 三種類型的值, 最簡單的作法就是直接使用 int64_t 類型的數組做爲整數集合的底層實現。不過這樣一來,就會出現浪費內存的狀況。
  • 整數集合如今的作法既可讓集合能同時保存三種不一樣類型的值, 又能夠確保升級操做只會在有須要的時候進行, 這能夠儘可能節省內存。

 

降級

  • 整數集合不支持降級操做, 一旦對數組進行了升級, 編碼就會一直保持升級後的狀態。
  • 舉個例子, 對於一個整數集合來講, 即便咱們將集合裏惟一一個真正須要使用 int64_t 類型來保存的元素 4294967295 刪除了, 整數集合的編碼仍然會維持 INTSET_ENC_INT64 , 底層數組也仍然會是 int64_t 類型的。

 

整數集合 API

表 6-1 列出了整數集合的操做 API 。

函數 做用 時間複雜度
intsetNew 建立一個新的整數集合。 O(1)
intsetAdd 將給定元素添加到整數集合裏面。 O(N)
intsetRemove 從整數集合中移除給定元素。 O(N)
intsetFind 檢查給定值是否存在於集合。 由於底層數組有序,查找能夠經過二分查找法來進行, 因此複雜度爲 O(\log N) 。
intsetRandom 從整數集合中隨機返回一個元素。 O(1)
intsetGet 取出底層數組在給定索引上的元素。 O(1)
intsetLen 返回整數集合包含的元素個數。 O(1)
intsetBlobLen 返回整數集合佔用的內存字節數。 O(1)

 

 


 

壓縮列表

導讀

  • 壓縮列表是一種爲節約內存而開發的順序型數據結構。
  • 壓縮列表被用做列表鍵哈希鍵的底層實現之一。
  • 壓縮列表能夠包含多個節點,每一個節點能夠保存一個字節數組或者整數值。
  • 添加新節點到壓縮列表, 或者從壓縮列表中刪除節點, 可能會引起連鎖更新操做, 但這種操做出現的概率並不高。

 

壓縮列表的構成

  • 壓縮列表是 Redis 爲了節約內存而開發的, 由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。
  • 一個壓縮列表能夠包含任意多個節點(entry), 每一個節點能夠保存一個字節數組或者一個整數值。

圖 7-1 展現了壓縮列表的各個組成部分, 表 7-1 則記錄了各個組成部分的類型、長度、以及用途。

 

 

表 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 三個部分組成, 如圖 7-4 所示。

 

 

  • 每一個壓縮列表節點能夠保存一個字節數組或者一個整數值, 其中, 字節數組能夠是如下三種長度的其中一種:
  1. 長度小於等於 63 (2^{6}-1)字節的字節數組;
  2. 長度小於等於 16383 (2^{14}-1) 字節的字節數組;
  3. 長度小於等於 4294967295 (2^{32}-1)字節的字節數組;
  • 而整數值則能夠是如下六種長度的其中一種:
  1. 4 位長,介於 0 至 12 之間的無符號整數;
  2. 1 字節長的有符號整數;
  3. 3 字節長的有符號整數;
  4. int16_t 類型整數;
  5. int32_t 類型整數;
  6. int64_t 類型整數。

 

previous_entry_length

  • 節點的 previous_entry_length 屬性以字節爲單位, 記錄了壓縮列表中前一個節點的長度。
  • previous_entry_length 屬性的長度能夠是 1 字節或者 5 字節:
  • 若是前一節點的長度小於 254 字節, 那麼 previous_entry_length 屬性的長度爲 1 字節: 前一節點的長度就保存在這一個字節裏面。
  • 若是前一節點的長度大於等於 254 字節, 那麼 previous_entry_length 屬性的長度爲 5 字節: 其中屬性的第一字節會被設置爲 0xFE (十進制值 254), 而以後的四個字節則用於保存前一節點的長度。
  • 由於節點的 previous_entry_length 屬性記錄了前一個節點的長度, 因此程序能夠經過指針運算, 根據當前節點的起始地址來計算出前一個節點的起始地址。

圖 7-5 展現了一個包含一字節長 previous_entry_length 屬性的壓縮列表節點, 屬性的值爲 0x05 , 表示前一節點的長度爲 5 字節。

圖 7-6 展現了一個包含五字節長 previous_entry_length 屬性的壓縮節點, 屬性的值爲 0xFE00002766 , 其中值的最高位字節 0xFE 表示這是一個五字節長的 previous_entry_length 屬性, 而以後的四字節 0x00002766 (十進制值 10086 )纔是前一節點的實際長度。

 

 

encoding

節點的 encoding 屬性記錄了節點的 content 屬性所保存數據的類型以及長度

  • 一字節、兩字節或者五字節長, 值的最高位爲 00 、 01 或者 10 的是字節數組編碼: 這種編碼表示節點的 content 屬性保存着字節數組, 數組的長度由編碼除去最高兩位以後的其餘位記錄;
  • 一字節長, 值的最高位以 11 開頭的是整數編碼: 這種編碼表示節點的 content 屬性保存着整數值, 整數值的類型和長度由編碼除去最高兩位以後的其餘位記錄;

表 7-2 記錄了全部可用的字節數組編碼, 而表 7-3 則記錄了全部可用的整數編碼。 表格中的下劃線 _ 表示留空, 而 b 、 x 等變量則表明實際的二進制數據, 爲了方便閱讀, 多個字節之間用空格隔開。

編碼 編碼長度 content 屬性保存的值
00bbbbbb 1 字節 長度小於等於 63 字節的字節數組。
01bbbbbb xxxxxxxx 2 字節 長度小於等於 16383 字節的字節數組。
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd 5 字節 長度小於等於 4294967295 的字節數組。

表 7-3 整數編碼

編碼 編碼長度 content 屬性保存的值
11000000 1 字節 int16_t 類型的整數。
11010000 1 字節 int32_t 類型的整數。
11100000 1 字節 int64_t 類型的整數。
11110000 1 字節 24 位有符號整數。
11111110 1 字節 8 位有符號整數。
1111xxxx 1 字節 使用這一編碼的節點沒有相應的 content 屬性, 由於編碼自己的 xxxx 四個位已經保存了一個介於 0 和 12 之間的值, 因此它無須 content 屬性。

 

content

  • 節點的 content 屬性負責保存節點的值, 節點值能夠是一個字節數組或者整數, 值的類型和長度由節點的 encoding 屬性決定
  • 圖 7-9 展現了一個保存字節數組的節點示例:
  • 編碼的最高兩位 00 表示節點保存的是一個字節數組;
  • 編碼的後六位 001011 記錄了字節數組的長度 11 ;
  • content 屬性保存着節點的值 "hello world" 。

  • 圖 7-10 展現了一個保存整數值的節點示例:
  • 編碼 11000000 表示節點保存的是一個 int16_t 類型的整數值;
  • content 屬性保存着節點的值 10086 。

 

連鎖更新

  • 添加新節點可能會引起連鎖更新以外,
  • 刪除節點也可能會引起連鎖更新。
  • 由於連鎖更新在最壞狀況下須要對壓縮列表執行 N 次空間重分配操做, 而每次空間重分配的最壞複雜度爲 O(N) , 因此連鎖更新的最壞複雜度爲 O(N^2) 
  • 要注意的是, 儘管連鎖更新的複雜度較高, 但它真正形成性能問題的概率是很低的:
  • 首先, 壓縮列表裏要剛好有多個連續的、長度介於 250 字節至 253 字節之間的節點, 連鎖更新纔有可能被引起, 在實際中, 這種狀況並很少見;
  • 其次, 即便出現連鎖更新, 但只要被更新的節點數量很少, 就不會對性能形成任何影響: 好比說, 對三五個節點進行連鎖更新是絕對不會影響性能的;

由於以上緣由, ziplistPush 等命令的平均複雜度僅爲 O(N) , 在實際中, 咱們能夠放心地使用這些函數, 而沒必要擔憂連鎖更新會影響壓縮列表的性能。

 

壓縮列表 API

表 7-4 列出了全部用於操做壓縮列表的 API 。

函數 做用 算法複雜度
ziplistNew 建立一個新的壓縮列表。 O(1)
ziplistPush 建立一個包含給定值的新節點, 並將這個新節點添加到壓縮列表的表頭或者表尾。 平均 O(N) ,最壞 O(N^2) 。
ziplistInsert 將包含給定值的新節點插入到給定節點以後。 平均 O(N) ,最壞 O(N^2) 。
ziplistIndex 返回壓縮列表給定索引上的節點。 O(N)
ziplistFind 在壓縮列表中查找並返回包含了給定值的節點。 由於節點的值多是一個字節數組, 因此檢查節點值和給定值是否相同的複雜度爲 O(N) , 而查找整個列表的複雜度則爲 O(N^2) 。
ziplistNext 返回給定節點的下一個節點。 O(1)
ziplistPrev 返回給定節點的前一個節點。 O(1)
ziplistGet 獲取給定節點所保存的值。 O(1)
ziplistDelete 從壓縮列表中刪除給定的節點。 平均 O(N) ,最壞 O(N^2) 。
ziplistDeleteRange 刪除壓縮列表在給定索引上的連續多個節點。 平均 O(N) ,最壞 O(N^2) 。
ziplistBlobLen 返回壓縮列表目前佔用的內存字節數。 O(1)
ziplistLen 返回壓縮列表目前包含的節點數量。 節點數量小於 65535 時 O(1) , 大於 65535 時 O(N) 。

由於 ziplistPush 、 ziplistInsert 、 ziplistDelete 和 ziplistDeleteRange 四個函數都有可能會引起連鎖更新, 因此它們的最壞複雜度都是 O(N^2) 。

 


 

對象

在前面的數個章節裏, 咱們陸續介紹了 Redis 用到的全部主要數據結構, 好比簡單動態字符串(SDS)、雙端鏈表、字典、壓縮列表、整數集合, 等等。

  • Redis 並無直接使用這些數據結構來實現鍵值對數據庫, 而是基於這些數據結構建立了一個對象系統, 這個系統包含字符串對象列表對象哈希對象集合對象有序集合對象這五種類型的對象, 每種對象都用到了至少一種咱們前面所介紹的數據結構
  • 經過這五種不一樣類型的對象,(1)Redis 能夠在執行命令以前, 根據對象的類型來判斷一個對象是否能夠執行給定的命令。 (2)能夠針對不一樣的使用場景, 爲對象設置多種不一樣的數據結構實現, 從而優化對象在不一樣場景下的使用效率。
  • Redis 的對象系統還實現了基於引用計數技術內存回收機制: 當程序再也不使用某個對象的時候, 這個對象所佔用的內存就會被自動釋放; 另外, Redis 還經過引用計數技術實現了對象共享機制, 這一機制能夠在適當的條件下, 經過讓多個數據庫鍵共享同一個對象來節約內存。
  • 最後, Redis 的對象帶有訪問時間記錄信息, 該信息能夠用於計算數據庫鍵的空轉時長, 在服務器啓用了 maxmemory 功能的狀況下, 空轉時長較大的那些鍵可能會優先被服務器刪除。

 

導讀

  • Redis 數據庫中的每一個鍵值對的鍵和值都是一個對象。
  • Redis 共有字符串、列表、哈希、集合、有序集合五種類型的對象, 每種類型的對象至少都有兩種或以上的編碼方式, 不一樣的編碼能夠在不一樣的使用場景上優化對象的使用效率。
  • 服務器在執行某些命令以前, 會先檢查給定鍵的類型可否執行指定的命令, 而檢查一個鍵的類型就是檢查鍵的值對象的類型。
  • Redis 的對象系統帶有引用計數實現的內存回收機制, 當一個對象再也不被使用時, 該對象所佔用的內存就會被自動釋放。
  • Redis 會共享值爲 0 到 9999 的字符串對象。
  • 對象會記錄本身的最後一次被訪問的時間, 這個時間能夠用於計算對象的空轉時間。

 

對象的類型與編碼

  • Redis 使用對象來表示數據庫中的鍵和值, 每次當咱們在 Redis 的數據庫中新建立一個鍵值對時, 咱們至少會建立兩個對象, 一個對象用做鍵值對的鍵(鍵對象), 另外一個對象用做鍵值對的值(值對象)。
  • Redis 中的每一個對象都由一個 redisObject 結構表示, 該結構中和保存數據有關的三個屬性分別是 type 屬性、 encoding 屬性和 ptr 屬性:
 1 typedef struct redisObject {
 2 
 3     // 類型
 4     unsigned type:4;
 5 
 6     // 編碼
 7     unsigned encoding:4;
 8 
 9     // 指向底層實現數據結構的指針
10     void *ptr;
11 
12     // ...
13 
14 } robj;

 

舉個例子, 如下 SET 命令在數據庫中建立了一個新的鍵值對, 其中鍵值對的鍵是一個包含了字符串值 "msg" 的對象, 而鍵值對的值則是一個包含了字符串值 "hello world" 的對象:

1 redis> SET msg "hello world"
2 OK

 

 

類型

  • 對象的 type 屬性記錄了對象的類型, 這個屬性的值能夠是如下常量的其中一個。

表 8-1 對象的類型

類型常量 對象的名稱
REDIS_STRING 字符串對象
REDIS_LIST 列表對象
REDIS_HASH 哈希對象
REDIS_SET 集合對象
REDIS_ZSET 有序集合對象
  • 對於 Redis 數據庫保存的鍵值對來講, 鍵老是一個字符串對象, 而值則能夠是字符串對象、列表對象、哈希對象、集合對象或者有序集合對象的其中一種, 所以:
  • 當咱們稱呼一個數據庫鍵爲「字符串鍵」時, 咱們指的是「這個數據庫鍵所對應的值爲字符串對象」;
  • 當咱們稱呼一個鍵爲「列表鍵」時, 咱們指的是「這個數據庫鍵所對應的值爲列表對象」,諸如此類。
  • TYPE 命令的實現方式也與此相似, 當咱們對一個數據庫鍵執行 TYPE 命令時, 命令返回的結果爲數據庫鍵對應的值對象的類型, 而不是鍵對象的類型:
1 # 鍵爲字符串對象,值爲列表對象
2 redis> RPUSH numbers 1 3 5
3 (integer) 6
4 
5 redis> TYPE numbers
6 list

 

表 8-2 列出了 TYPE 命令在面對不一樣類型的值對象時所產生的輸出。

對象 對象 type 屬性的值 TYPE 命令的輸出
字符串對象 REDIS_STRING "string"
列表對象 REDIS_LIST "list"
哈希對象 REDIS_HASH "hash"
集合對象 REDIS_SET "set"
有序集合對象 REDIS_ZSET "zset"

 

編碼和底層實現

  1. 對象的 ptr 指針指向對象的底層實現數據結構, 而這些數據結構由對象的 encoding 屬性決定。

encoding 屬性記錄了對象所使用的編碼, 也便是說這個對象使用了什麼數據結構做爲對象的底層實現, 這個屬性的值能夠是表 8-3 列出的常量的其中一個。

編碼常量 編碼所對應的底層數據結構 OBJECT ENCODING 命令輸出
REDIS_ENCODING_INT long 類型的整數 "int"
REDIS_ENCODING_EMBSTR embstr 編碼的簡單動態字符串 "embstr"
REDIS_ENCODING_RAW 簡單動態字符串 "raw"
REDIS_ENCODING_HT 字典 "hashtable"
REDIS_ENCODING_LINKEDLIST 雙端鏈表 "linkedlist"
REDIS_ENCODING_ZIPLIST 壓縮列表 "ziplist"
REDIS_ENCODING_INTSET 整數集合 "intset"
REDIS_ENCODING_SKIPLIST 跳躍表和字典 "skiplist"
  1. 其中,每種type類型的對象都至少使用了兩種不一樣的編碼, 表 8-4 不一樣類型和編碼的對象
類型常量 編碼 對象
REDIS_STRING REDIS_ENCODING_INT 使用整數值實現的字符串對象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 編碼的簡單動態字符串實現的字符串對象。
REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字符串實現的字符串對象。
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的列表對象。
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實現的列表對象。
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的哈希對象。
REDIS_HASH REDIS_ENCODING_HT 使用字典實現的哈希對象。
REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實現的集合對象。
REDIS_SET REDIS_ENCODING_HT 使用字典實現的集合對象。
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的有序集合對象。
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳躍表和字典實現的有序集合對象。

使用 OBJECT ENCODING 命令能夠查看一個數據庫鍵的值對象的編碼:

 1 redis> SET msg "hello wrold"
 2 OK
 3 
 4 redis> OBJECT ENCODING msg
 5 "embstr"
 6 
 7 redis> SET story "long long long long long long ago ..."
 8 OK
 9 
10 redis> OBJECT ENCODING story
11 "raw"
12 
13 redis> SADD numbers 1 3 5
14 (integer) 3
15 
16 redis> OBJECT ENCODING numbers
17 "intset"
18 
19 redis> SADD numbers "seven"
20 (integer) 1
21 
22 redis> OBJECT ENCODING numbers
23 "hashtable"

 

  1. 經過 encoding 屬性來設定對象所使用的編碼, 而不是爲特定類型的對象關聯一種固定的編碼, 極大地提高了 Redis 的靈活性和效率, 由於 Redis 能夠根據不一樣的使用場景來爲一個對象設置不一樣的編碼, 從而優化對象在某一場景下的效率。

舉個例子, 在列表對象包含的元素比較少時, Redis 使用壓縮列表做爲列表對象的底層實現:

  • 由於壓縮列表比雙端鏈表更節約內存, 而且在元素數量較少時, 在內存中以連續塊方式保存的壓縮列表比起雙端鏈表能夠更快被載入到緩存中;
  • 隨着列表對象包含的元素愈來愈多, 使用壓縮列表來保存元素的優點逐漸消失時, 對象就會將底層實現從壓縮列表轉向功能更強、也更適合保存大量元素的雙端鏈表上面;

其餘類型的對象也會經過使用多種不一樣的編碼來進行相似的優化。

在接下來的內容中, 咱們將分別介紹 Redis 中的五種不一樣類型的對象, 說明這些對象底層所使用的編碼方式, 列出對象從一種編碼轉換成另外一種編碼所需的條件, 以及同一個命令在多種不一樣編碼上的實現方法。

 

字符串對象

  • 字符串對象的編碼能夠是 int 、 raw 或者 embstr 。
  • 若是一個字符串對象保存的是整數值, 而且這個整數值能夠用 long 類型來表示, 那麼字符串對象會將整數值保存在字符串對象結構的 ptr 屬性裏面(將 void* 轉換成 long ), 並將字符串對象的編碼設置爲 int 。

舉個例子, 若是咱們執行如下 SET 命令, 那麼服務器將建立一個如圖 8-1 所示的 int 編碼的字符串對象做爲 number 鍵的值:

1 redis> SET number 10086
2 OK
3 
4 redis> OBJECT ENCODING number
5 "int"

 

 

  • 若是字符串對象保存的是一個字符串值, 而且這個字符串值的長度大於 39 字節, 那麼字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值, 並將對象的編碼設置爲 raw 。

舉個例子, 若是咱們執行如下命令, 那麼服務器將建立一個如圖 8-2 所示的 raw 編碼的字符串對象做爲 story 鍵的值:

1 redis> SET story "Long, long, long ago there lived a king ..."
2 OK
3 
4 redis> STRLEN story
5 (integer) 43
6 
7 redis> OBJECT ENCODING story
8 "raw"

 

 

  • 若是字符串對象保存的是一個字符串值, 而且這個字符串值的長度小於等於 39 字節, 那麼字符串對象將使用 embstr 編碼的方式來保存這個字符串值。

embstr 編碼是專門用於保存短字符串的一種優化編碼方式, 這種編碼和 raw 編碼同樣, 都使用 redisObject 結構和 sdshdr 結構來表示字符串對象, 但 raw 編碼會調用兩次內存分配函數來分別建立 redisObject 結構和 sdshdr 結構, 而 embstr 編碼則經過調用一次內存分配函數來分配一塊連續的空間, 空間中依次包含 redisObject 和 sdshdr 兩個結構, 如圖 8-3 所示。

 

embstr 編碼的字符串對象在執行命令時, 產生的效果和 raw 編碼的字符串對象執行命令時產生的效果是相同的, 但使用 embstr 編碼的字符串對象來保存短字符串值有如下好處:

  1. embstr 編碼將建立字符串對象所需的內存分配次數從 raw 編碼的兩次下降爲一次。
  2. 釋放 embstr 編碼的字符串對象只須要調用一次內存釋放函數, 而釋放 raw 編碼的字符串對象須要調用兩次內存釋放函數。
  3. 由於 embstr 編碼的字符串對象的全部數據都保存在一塊連續的內存裏面, 因此這種編碼的字符串對象比起 raw 編碼的字符串對象可以更好地利用緩存帶來的優點。

做爲例子, 如下命令建立了一個 embstr 編碼的字符串對象做爲 msg 鍵的值, 值對象的樣子如圖 8-4 所示:

1 redis> SET msg "hello"
2 OK
3 
4 redis> OBJECT ENCODING msg
5 "embstr"

 

 

 

  • 最後要說的是, 能夠用 long double 類型表示的浮點數在 Redis 中也是做爲字符串值來保存的: 若是咱們要保存一個浮點數到字符串對象裏面, 那麼程序會先將這個浮點數轉換成字符串值, 而後再保存起轉換所得的字符串值。在有須要的時候, 程序會將保存在字符串對象裏面的字符串值轉換回浮點數值, 執行某些操做, 而後再將執行操做所得的浮點數值轉換回字符串值, 並繼續保存在字符串對象裏面。

表 8-6 字符串對象保存各種型值的編碼方式

編碼
能夠用 long 類型保存的整數。 int
能夠用 long double 類型保存的浮點數。 embstr 或者 raw
字符串值, 或者由於長度太大而沒辦法用 long 類型表示的整數, 又或者由於長度太大而沒辦法用 long double 類型表示的浮點數。 embstr 或者 raw

 

編碼的轉換

  • int 編碼的字符串對象和 embstr 編碼的字符串對象在條件知足的狀況下, 會被轉換爲 raw 編碼的字符串對象。
  • 對於 int 編碼的字符串對象來講, 若是咱們向對象執行了一些命令, 使得這個對象保存的再也不是整數值, 而是一個字符串值, 那麼字符串對象的編碼將從 int 變爲 raw 。好比APPEND 命令
  • 另外, 由於 Redis 沒有爲 embstr 編碼的字符串對象編寫任何相應的修改程序 (只有 int 編碼的字符串對象和 raw 編碼的字符串對象有這些程序), 因此 embstr 編碼的字符串對象其實是隻讀的: 當咱們對 embstr 編碼的字符串對象執行任何修改命令時, 程序會先將對象的編碼從 embstr 轉換成 raw , 而後再執行修改命令; 由於這個緣由, embstr 編碼的字符串對象在執行修改命令以後, 總會變成一個 raw 編碼的字符串對象。

 

字符串命令的實現

由於字符串鍵的值爲字符串對象, 因此用於字符串鍵的全部命令都是針對字符串對象來構建的, 表 8-7 列舉了其中一部分字符串命令, 以及這些命令在不一樣編碼的字符串對象下的實現方法。

命令 int 編碼的實現方法 embstr 編碼的實現方法 raw 編碼的實現方法
SET 使用 int 編碼保存值。 使用 embstr 編碼保存值。 使用 raw 編碼保存值。
GET 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 而後向客戶端返回這個字符串值。 直接向客戶端返回字符串值。 直接向客戶端返回字符串值。
APPEND 將對象轉換成 raw 編碼, 而後按 raw 編碼的方式執行此操做。 將對象轉換成 raw 編碼, 而後按 raw 編碼的方式執行此操做。 調用 sdscatlen 函數, 將給定字符串追加到現有字符串的末尾。
INCRBYFLOAT 取出整數值並將其轉換成 long double 類型的浮點數, 對這個浮點數進行加法計算, 而後將得出的浮點數結果保存起來。 取出字符串值並嘗試將其轉換成 long double 類型的浮點數, 對這個浮點數進行加法計算, 而後將得出的浮點數結果保存起來。 若是字符串值不能被轉換成浮點數, 那麼向客戶端返回一個錯誤。 取出字符串值並嘗試將其轉換成 long double 類型的浮點數, 對這個浮點數進行加法計算, 而後將得出的浮點數結果保存起來。 若是字符串值不能被轉換成浮點數, 那麼向客戶端返回一個錯誤。
INCRBY 對整數值進行加法計算, 得出的計算結果會做爲整數被保存起來。 embstr 編碼不能執行此命令, 向客戶端返回一個錯誤。 raw 編碼不能執行此命令, 向客戶端返回一個錯誤。
DECRBY 對整數值進行減法計算, 得出的計算結果會做爲整數被保存起來。 embstr 編碼不能執行此命令, 向客戶端返回一個錯誤。 raw 編碼不能執行此命令, 向客戶端返回一個錯誤。
STRLEN 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 計算並返回這個字符串值的長度。 調用 sdslen 函數, 返回字符串的長度。 調用 sdslen 函數, 返回字符串的長度。
SETRANGE 將對象轉換成 raw 編碼, 而後按 raw 編碼的方式執行此命令。 將對象轉換成 raw 編碼, 而後按 raw 編碼的方式執行此命令。 將字符串特定索引上的值設置爲給定的字符。
GETRANGE 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 而後取出並返回字符串指定索引上的字符。 直接取出並返回字符串指定索引上的字符。  

 

列表對象

  • 列表對象的編碼能夠是 ziplist 或者 linkedlist 。
  • ziplist 編碼的列表對象使用壓縮列表做爲底層實現, 每一個壓縮列表節點(entry)保存了一個列表元素。
  • 另外一方面, linkedlist 編碼的列表對象使用雙端鏈表做爲底層實現, 每一個雙端鏈表節點(node)都保存了一個字符串對象, 而每一個字符串對象都保存了一個列表元素。

舉個例子, 若是咱們執行如下 RPUSH 命令, 那麼服務器將建立一個列表對象做爲 numbers 鍵的值:

1 redis> RPUSH numbers 1 "three" 5
2 (integer) 3

 

 

 

 

 

 注意, linkedlist 編碼的列表對象在底層的雙端鏈表結構中包含了多個字符串對象, 這種嵌套字符串對象的行爲在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現, 字符串對象是 Redis 五種類型的對象中惟一一種會被其餘四種類型對象嵌套的對象。

注意

爲了簡化字符串對象的表示, 咱們在圖 8-6 使用了一個帶有 StringObject 字樣的格子來表示一個字符串對象, 而 StringObject 字樣下面的是字符串對象所保存的值。

好比說, 圖 8-7 表明的就是一個包含了字符串值 "three" 的字符串對象, 它是 8-8 的簡化表示。

本書接下來的內容將繼續沿用這一簡化表示。

 

編碼轉換

當列表對象能夠同時知足如下兩個條件時, 列表對象使用 ziplist 編碼:

  1. 列表對象保存的全部字符串元素的長度都小於 64 字節
  2. 列表對象保存的元素數量小於 512 個

不能知足這兩個條件的列表對象須要使用 linkedlist 編碼。

  • 對於使用 ziplist 編碼的列表對象來講, 當使用 ziplist 編碼所需的兩個條件的任意一個不能被知足時, 對象的編碼轉換操做就會被執行: 本來保存在壓縮列表裏的全部列表元素都會被轉移並保存到雙端鏈表裏面, 對象的編碼也會從 ziplist 變爲 linkedlist 。

注意

以上兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 list-max-ziplist-value 選項和 list-max-ziplist-entries 選項的說明。

 

列表命令的實現

由於列表鍵的值爲列表對象, 因此用於列表鍵的全部命令都是針對列表對象來構建的,

表 8-8 列出了其中一部分列表鍵命令, 以及這些命令在不一樣編碼的列表對象下的實現方法。

命令 ziplist 編碼的實現方法 linkedlist 編碼的實現方法
LPUSH 調用 ziplistPush 函數, 將新元素推入到壓縮列表的表頭。 調用 listAddNodeHead 函數, 將新元素推入到雙端鏈表的表頭。
RPUSH 調用 ziplistPush 函數, 將新元素推入到壓縮列表的表尾。 調用 listAddNodeTail 函數, 將新元素推入到雙端鏈表的表尾。
LPOP 調用 ziplistIndex 函數定位壓縮列表的表頭節點, 在向用戶返回節點所保存的元素以後, 調用 ziplistDelete 函數刪除表頭節點。 調用 listFirst 函數定位雙端鏈表的表頭節點, 在向用戶返回節點所保存的元素以後, 調用 listDelNode 函數刪除表頭節點。
RPOP 調用 ziplistIndex 函數定位壓縮列表的表尾節點, 在向用戶返回節點所保存的元素以後, 調用 ziplistDelete 函數刪除表尾節點。 調用 listLast 函數定位雙端鏈表的表尾節點, 在向用戶返回節點所保存的元素以後, 調用 listDelNode 函數刪除表尾節點。
LINDEX 調用 ziplistIndex 函數定位壓縮列表中的指定節點, 而後返回節點所保存的元素。 調用 listIndex 函數定位雙端鏈表中的指定節點, 而後返回節點所保存的元素。
LLEN 調用 ziplistLen 函數返回壓縮列表的長度。 調用 listLength 函數返回雙端鏈表的長度。
LINSERT 插入新節點到壓縮列表的表頭或者表尾時, 使用 ziplistPush 函數; 插入新節點到壓縮列表的其餘位置時, 使用 ziplistInsert 函數。 調用 listInsertNode 函數, 將新節點插入到雙端鏈表的指定位置。
LREM 遍歷壓縮列表節點, 並調用 ziplistDelete 函數刪除包含了給定元素的節點。 遍歷雙端鏈表節點, 並調用 listDelNode 函數刪除包含了給定元素的節點。
LTRIM 調用 ziplistDeleteRange 函數, 刪除壓縮列表中全部不在指定索引範圍內的節點。 遍歷雙端鏈表節點, 並調用 listDelNode 函數刪除鏈表中全部不在指定索引範圍內的節點。
LSET 調用 ziplistDelete 函數, 先刪除壓縮列表指定索引上的現有節點, 而後調用 ziplistInsert 函數, 將一個包含給定元素的新節點插入到相同索引上面。 調用 listIndex 函數, 定位到雙端鏈表指定索引上的節點, 而後經過賦值操做更新節點的值。

 

哈希對象

  • 哈希對象的編碼能夠是 ziplist 或者 hashtable 
  • ziplist 編碼的哈希對象使用壓縮列表做爲底層實現, 每當有新的鍵值對要加入到哈希對象時, 程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾, 而後再將保存了值的壓縮列表節點推入到壓縮列表表尾, 所以:
    • 保存了同一鍵值對的兩個節點老是緊挨在一塊兒, 保存鍵的節點在前, 保存值的節點在後;
    • 先添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向, 然後來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。
  • 另外一方面, hashtable 編碼的哈希對象使用字典做爲底層實現, 哈希對象中的每一個鍵值對都使用一個字典鍵值對來保存:
    • 字典的每一個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵;
    • 字典的每一個值都是一個字符串對象, 對象中保存了鍵值對的值。

舉個例子, 若是咱們執行如下 HSET 命令, 那麼服務器將建立一個列表對象做爲 profile 鍵的值:

1 redis> HSET profile name "Tom"
2 (integer) 1
3 
4 redis> HSET profile age 25
5 (integer) 1
6 
7 redis> HSET profile career "Programmer"
8 (integer) 1

 

 

 

 

 

 

編碼轉換

當哈希對象能夠同時知足如下兩個條件時, 哈希對象使用 ziplist 編碼:

  1. 哈希對象保存的全部鍵值對的鍵和值的字符串長度都小於 64 字節
  2. 哈希對象保存的鍵值對數量小於 512 個

不能知足這兩個條件的哈希對象須要使用 hashtable 編碼。

  • 對於使用 ziplist 編碼的列表對象來講, 當使用 ziplist 編碼所需的兩個條件的任意一個不能被知足時, 對象的編碼轉換操做就會被執行: 本來保存在壓縮列表裏的全部鍵值對都會被轉移並保存到字典裏面, 對象的編碼也會從 ziplist 變爲 hashtable 。

注意

這兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 hash-max-ziplist-value 選項和 hash-max-ziplist-entries 選項的說明。

 

哈希命令的實現

由於哈希鍵的值爲哈希對象, 因此用於哈希鍵的全部命令都是針對哈希對象來構建的, 表 8-9 列出了其中一部分哈希鍵命令, 以及這些命令在不一樣編碼的哈希對象下的實現方法。

命令 ziplist 編碼實現方法 hashtable 編碼的實現方法
HSET 首先調用 ziplistPush 函數, 將鍵推入到壓縮列表的表尾, 而後再次調用 ziplistPush 函數, 將值推入到壓縮列表的表尾。 調用 dictAdd 函數, 將新節點添加到字典裏面。
HGET 首先調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 而後調用 ziplistNext 函數, 將指針移動到鍵節點旁邊的值節點, 最後返回值節點。 調用 dictFind 函數, 在字典中查找給定鍵, 而後調用 dictGetVal 函數, 返回該鍵所對應的值。
HEXISTS 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 若是找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。 調用 dictFind 函數, 在字典中查找給定鍵, 若是找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。
HDEL 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 而後將相應的鍵節點、 以及鍵節點旁邊的值節點都刪除掉。 調用 dictDelete 函數, 將指定鍵所對應的鍵值對從字典中刪除掉。
HLEN 調用 ziplistLen 函數, 取得壓縮列表包含節點的總數量, 將這個數量除以 2 , 得出的結果就是壓縮列表保存的鍵值對的數量。 調用 dictSize 函數, 返回字典包含的鍵值對數量, 這個數量就是哈希對象包含的鍵值對數量。
HGETALL 遍歷整個壓縮列表, 用 ziplistGet 函數返回全部鍵和值(都是節點)。 遍歷整個字典, 用 dictGetKey 函數返回字典的鍵, 用 dictGetVal 函數返回字典的值。

 

集合對象

  • 集合對象的編碼能夠是 intset 或者 hashtable 。
  • intset 編碼的集合對象使用整數集合做爲底層實現, 集合對象包含的全部元素都被保存在整數集合裏面。
  • 另外一方面, hashtable 編碼的集合對象使用字典做爲底層實現, 字典的每一個鍵都是一個字符串對象, 每一個字符串對象包含了一個集合元素, 而字典的值則所有被設置爲 NULL 。

舉個例子, 如下代碼將建立一個如圖 8-12 所示的 intset 編碼集合對象:

1 redis> SADD numbers 1 3 5
2 (integer) 3

 

 

 

如下代碼將建立一個如圖 8-13 所示的 hashtable 編碼集合對象:

1 redis> SADD fruits "apple" "banana" "cherry"
2 (integer) 3

 

 

 

 

 

編碼的轉換

當集合對象能夠同時知足如下兩個條件時, 對象使用 intset 編碼:

  1. 集合對象保存的全部元素都是整數值;
  2. 集合對象保存的元素數量不超過 512 個;

不能知足這兩個條件的集合對象須要使用 hashtable 編碼。

  • 對於使用 intset 編碼的集合對象來講, 當使用 intset 編碼所需的兩個條件的任意一個不能被知足時, 對象的編碼轉換操做就會被執行: 本來保存在整數集合中的全部元素都會被轉移並保存到字典裏面, 而且對象的編碼也會從 intset 變爲 hashtable 。

注意

第二個條件的上限值是能夠修改的, 具體請看配置文件中關於 set-max-intset-entries 選項的說明。

 

集合命令的實現

由於集合鍵的值爲集合對象, 因此用於集合鍵的全部命令都是針對集合對象來構建的, 表 8-10 列出了其中一部分集合鍵命令, 以及這些命令在不一樣編碼的集合對象下的實現方法。

表 8-10 集合命令的實現方法

命令 intset 編碼的實現方法 hashtable 編碼的實現方法
SADD 調用 intsetAdd 函數, 將全部新元素添加到整數集合裏面。 調用 dictAdd , 以新元素爲鍵, NULL 爲值, 將鍵值對添加到字典裏面。
SCARD 調用 intsetLen 函數, 返回整數集合所包含的元素數量, 這個數量就是集合對象所包含的元素數量。 調用 dictSize 函數, 返回字典所包含的鍵值對數量, 這個數量就是集合對象所包含的元素數量。
SISMEMBER 調用 intsetFind 函數, 在整數集合中查找給定的元素, 若是找到了說明元素存在於集合, 沒找到則說明元素不存在於集合。 調用 dictFind 函數, 在字典的鍵中查找給定的元素, 若是找到了說明元素存在於集合, 沒找到則說明元素不存在於集合。
SMEMBERS 遍歷整個整數集合, 使用 intsetGet 函數返回集合元素。 遍歷整個字典, 使用 dictGetKey 函數返回字典的鍵做爲集合元素。
SRANDMEMBER 調用 intsetRandom 函數, 從整數集合中隨機返回一個元素。 調用 dictGetRandomKey 函數, 從字典中隨機返回一個字典鍵。
SPOP 調用 intsetRandom 函數, 從整數集合中隨機取出一個元素, 在將這個隨機元素返回給客戶端以後, 調用 intsetRemove 函數, 將隨機元素從整數集合中刪除掉。 調用 dictGetRandomKey 函數, 從字典中隨機取出一個字典鍵, 在將這個隨機字典鍵的值返回給客戶端以後, 調用 dictDelete 函數, 從字典中刪除隨機字典鍵所對應的鍵值對。
SREM 調用 intsetRemove 函數, 從整數集合中刪除全部給定的元素。 調用 dictDelete 函數, 從字典中刪除全部鍵爲給定元素的鍵值對。

 

有序集合對象

  • 有序集合的編碼能夠是 ziplist 或者 skiplist 。
  • ziplist 編碼的有序集合對象使用壓縮列表做爲底層實現, 每一個集合元素使用兩個緊挨在一塊兒的壓縮列表節點來保存, 第一個節點保存元素的成員(member), 而第二個元素則保存元素的分值(score)。
  • 壓縮列表內的集合元素按分值從小到大進行排序, 分值較小的元素被放置在靠近表頭的方向, 而分值較大的元素則被放置在靠近表尾的方向。
  • skiplist 編碼的有序集合對象使用 zset 結構做爲底層實現, 一個 zset 結構同時包含一個字典和一個跳躍表:
1 typedef struct zset {
2     
3     zskiplist *zsl;
4     dict *dict;
5     
6 } zset;

 

    • zset 結構中的 zsl 跳躍表按分值從小到大保存了全部集合元素, 每一個跳躍表節點都保存了一個集合元素: 跳躍表節點的 object 屬性保存了元素的成員, 而跳躍表節點的 score 屬性則保存了元素的分值。 經過這個跳躍表, 程序能夠對有序集合進行範圍型操做, 好比 ZRANK 、 ZRANGE 等命令就是基於跳躍表 API 來實現的。
    • zset 結構中的 dict 字典爲有序集合建立了一個從成員到分值的映射, 字典中的每一個鍵值對都保存了一個集合元素: 字典的鍵保存了元素的成員, 而字典的值則保存了元素的分值。 經過這個字典, 程序能夠用 O(1) 複雜度查找給定成員的分值, ZSCORE 命令就是根據這一特性實現的, 而不少其餘有序集合命令都在實現的內部用到了這一特性。
    • 值得一提的是, 雖然 zset 結構同時使用跳躍表和字典來保存有序集合元素, 但這兩種數據結構都會經過指針來共享相同元素的成員和分值, 因此同時使用跳躍表和字典來保存集合元素不會產生任何重複成員或者分值, 也不會所以而浪費額外的內存。
  • 有序集合每一個元素的成員都是一個字符串對象, 而每一個元素的分值都是一個 double 類型的浮點數。

舉個例子, 若是咱們執行如下 ZADD 命令, 那麼服務器將建立一個有序集合對象做爲 price 鍵的值:

1 redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
2 (integer) 3

 

  • 若是 price 鍵的值對象使用的是 ziplist 編碼, 那麼這個值對象將會是圖 8-14 所示的樣子, 而對象所使用的壓縮列表則會是 8-15 所示的樣子。

 

 

  • 若是前面 price 鍵建立的不是 ziplist 編碼的有序集合對象, 而是 skiplist 編碼的有序集合對象, 那麼這個有序集合對象將會是圖 8-16 所示的樣子, 而對象所使用的 zset 結構將會是圖 8-17 所示的樣子。

 

 

  

 注意

爲了展現方便, 圖 8-17 在字典和跳躍表中重複展現了各個元素的成員和分值, 但在實際中, 字典和跳躍表會共享元素的成員和分值, 因此並不會形成任何數據重複, 也不會所以而浪費任何內存。

 

爲何有序集合須要同時使用跳躍表和字典來實現?

  • 在理論上來講, 有序集合能夠單獨使用字典或者跳躍表的其中一種數據結構來實現, 但不管單獨使用字典仍是跳躍表, 在性能上對比起同時使用字典和跳躍表都會有所下降。
  • 舉個例子, 若是咱們只使用字典來實現有序集合, 那麼雖然以 O(1) 複雜度查找成員的分值這一特性會被保留, 可是, 由於字典以無序的方式來保存集合元素, 因此每次在執行範圍型操做 —— 好比 ZRANK 、 ZRANGE 等命令時, 程序都須要對字典保存的全部元素進行排序, 完成這種排序須要至少 O(N \log N) 時間複雜度, 以及額外的 O(N) 內存空間 (由於要建立一個數組來保存排序後的元素)。
  • 另外一方面, 若是咱們只使用跳躍表來實現有序集合, 那麼跳躍表執行範圍型操做的全部優勢都會被保留, 但由於沒有了字典, 因此根據成員查找分值這一操做的複雜度將從 O(1) 上升爲 O(\log N) 。
  • 由於以上緣由, 爲了讓有序集合的查找和範圍型操做都儘量快地執行, Redis 選擇了同時使用字典和跳躍表兩種數據結構來實現有序集合。

 

編碼的轉換

當有序集合對象能夠同時知足如下兩個條件時, 對象使用 ziplist 編碼:

  1. 有序集合保存的元素數量小於 128 個;
  2. 有序集合保存的全部元素成員的長度都小於 64 字節;

不能知足以上兩個條件的有序集合對象將使用 skiplist 編碼。

  • 對於使用 ziplist 編碼的有序集合對象來講, 當使用 ziplist 編碼所需的兩個條件中的任意一個不能被知足時, 程序就會執行編碼轉換操做, 將本來儲存在壓縮列表裏面的全部集合元素轉移到 zset 結構裏面, 並將對象的編碼從 ziplist 改成 skiplist 。

注意

以上兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 zset-max-ziplist-entries 選項和 zset-max-ziplist-value 選項的說明。

 

有序集合命令的實現

由於有序集合鍵的值爲有序集合對象, 因此用於有序集合鍵的全部命令都是針對有序集合對象來構建的, 表 8-11 列出了其中一部分有序集合鍵命令, 以及這些命令在不一樣編碼的有序集合對象下的實現方法。

命令 ziplist 編碼的實現方法 zset 編碼的實現方法
ZADD 調用 ziplistInsert 函數, 將成員和分值做爲兩個節點分別插入到壓縮列表。 先調用 zslInsert 函數, 將新元素添加到跳躍表, 而後調用 dictAdd 函數, 將新元素關聯到字典。
ZCARD 調用 ziplistLen 函數, 得到壓縮列表包含節點的數量, 將這個數量除以 2 得出集合元素的數量。 訪問跳躍表數據結構的 length 屬性, 直接返回集合元素的數量。
ZCOUNT 遍歷壓縮列表, 統計分值在給定範圍內的節點的數量。 遍歷跳躍表, 統計分值在給定範圍內的節點的數量。
ZRANGE 從表頭向表尾遍歷壓縮列表, 返回給定索引範圍內的全部元素。 從表頭向表尾遍歷跳躍表, 返回給定索引範圍內的全部元素。
ZREVRANGE 從表尾向表頭遍歷壓縮列表, 返回給定索引範圍內的全部元素。 從表尾向表頭遍歷跳躍表, 返回給定索引範圍內的全部元素。
ZRANK 從表頭向表尾遍歷壓縮列表, 查找給定的成員, 沿途記錄通過節點的數量, 當找到給定成員以後, 途經節點的數量就是該成員所對應元素的排名。 從表頭向表尾遍歷跳躍表, 查找給定的成員, 沿途記錄通過節點的數量, 當找到給定成員以後, 途經節點的數量就是該成員所對應元素的排名。
ZREVRANK 從表尾向表頭遍歷壓縮列表, 查找給定的成員, 沿途記錄通過節點的數量, 當找到給定成員以後, 途經節點的數量就是該成員所對應元素的排名。 從表尾向表頭遍歷跳躍表, 查找給定的成員, 沿途記錄通過節點的數量, 當找到給定成員以後, 途經節點的數量就是該成員所對應元素的排名。
ZREM 遍歷壓縮列表, 刪除全部包含給定成員的節點, 以及被刪除成員節點旁邊的分值節點。 遍歷跳躍表, 刪除全部包含了給定成員的跳躍表節點。 並在字典中解除被刪除元素的成員和分值的關聯。
ZSCORE 遍歷壓縮列表, 查找包含了給定成員的節點, 而後取出成員節點旁邊的分值節點保存的元素分值。 直接從字典中取出給定成員的分值。

 

類型檢查與命令多態

  • Redis 中用於操做鍵的命令基本上能夠分爲兩種類型。
  • 其中一種命令能夠對任何類型的鍵執行, 好比說 DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令, 等等。
  • 而另外一種命令只能對特定類型的鍵執行, 好比說:
    • SET 、 GET 、 APPEND 、 STRLEN 等命令只能對字符串鍵執行;
    • HDEL 、 HSET 、 HGET 、 HLEN 等命令只能對哈希鍵執行;
    • RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能對列表鍵執行;
    • SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能對集合鍵執行;
    • ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能對有序集合鍵執行;

例子1, 如下代碼就展現了使用 DEL 命令來刪除三種不一樣類型的鍵:

 1 # 字符串鍵
 2 redis> SET msg "hello"
 3 OK
 4 
 5 # 列表鍵
 6 redis> RPUSH numbers 1 2 3
 7 (integer) 3
 8 
 9 # 集合鍵
10 redis> SADD fruits apple banana cherry
11 (integer) 3
12 
13 redis> DEL msg
14 (integer) 1
15 
16 redis> DEL numbers
17 (integer) 1
18 
19 redis> DEL fruits
20 (integer) 1

 

例子2, 咱們能夠用 SET 命令建立一個字符串鍵, 而後用 GET 命令和 APPEND 命令操做這個鍵, 但若是咱們試圖對這個字符串鍵執行只有列表鍵才能執行的 LLEN 命令, 那麼 Redis 將向咱們返回一個類型錯誤:

 1 redis> SET msg "hello world"
 2 OK
 3 
 4 redis> GET msg
 5 "hello world"
 6 
 7 redis> APPEND msg " again!"
 8 (integer) 18
 9 
10 redis> GET msg
11 "hello world again!"
12 
13 redis> LLEN msg
14 (error) WRONGTYPE Operation against a key holding the wrong kind of value

 

 

類型檢查的實現

從上面發生類型錯誤的代碼示例能夠看出, 爲了確保只有指定類型的鍵能夠執行某些特定的命令, 在執行一個類型特定的命令以前, Redis 會先檢查輸入鍵的類型是否正確, 而後再決定是否執行給定的命令。

類型特定命令所進行的類型檢查是經過 redisObject 結構的 type 屬性來實現的:

  1. 在執行一個類型特定命令以前, 服務器會先檢查輸入數據庫鍵的值對象是否爲執行命令所需的類型, 若是是的話, 服務器就對鍵執行指定的命令;
  2. 不然, 服務器將拒絕執行命令, 並向客戶端返回一個類型錯誤。

舉個例子, 對於 LLEN 命令來講:

  1. 在執行 LLEN 命令以前, 服務器會先檢查輸入數據庫鍵的值對象是否爲列表類型, 也便是, 檢查值對象 redisObject 結構 type 屬性的值是否爲 REDIS_LIST , 若是是的話, 服務器就對鍵執行 LLEN 命令;
  2. 不然的話, 服務器就拒絕執行命令並向客戶端返回一個類型錯誤;

 

 

 其餘類型特定命令的類型檢查過程也和這裏展現的 LLEN 命令的類型檢查過程相似。

 

多態命令的實現

  • Redis 除了會根據值對象的類型來判斷鍵是否可以執行指定命令以外, 還會根據值對象的編碼方式, 選擇正確的命令實現代碼來執行命令。
  • 舉個例子, 在前面介紹列表對象的編碼時咱們說過, 列表對象有 ziplist 和 linkedlist 兩種編碼可用, 其中前者使用壓縮列表 API 來實現列表命令, 然後者則使用雙端鏈表 API 來實現列表命令。

如今, 考慮這樣一個狀況, 若是咱們對一個鍵執行 LLEN 命令, 那麼服務器除了要確保執行命令的是列表鍵以外, 還須要根據鍵的值對象所使用的編碼來選擇正確的 LLEN 命令實現:

  • 若是列表對象的編碼爲 ziplist , 那麼說明列表對象的實現爲壓縮列表, 程序將使用 ziplistLen 函數來返回列表的長度;
  • 若是列表對象的編碼爲 linkedlist , 那麼說明列表對象的實現爲雙端鏈表, 程序將使用 listLength 函數來返回雙端鏈表的長度;

借用面向對象方面的術語來講, 咱們能夠認爲 LLEN 命令是多態(polymorphism)的: 只要執行 LLEN 命令的是列表鍵, 那麼不管值對象使用的是 ziplist 編碼仍是 linkedlist 編碼, 命令均可以正常執行。

圖 8-19 其餘類型特定命令的執行過程也是相似的。

 

 

 實際上, 咱們能夠將 DEL 、 EXPIRE 、 TYPE 等命令也稱爲多態命令, 由於不管輸入的鍵是什麼類型, 這些命令均可以正確地執行。他們和 LLEN 等命令的區別在於, 前者是基於類型的多態 —— 一個命令能夠同時用於處理多種不一樣類型的鍵, 而後者是基於編碼的多態 —— 一個命令能夠同時用於處理多種不一樣編碼。

 

內存回收

  • 由於 C 語言並不具有自動的內存回收功能, 因此 Redis 在本身的對象系統中構建了一個引用計數reference counting)技術實現的內存回收機制, 經過這一機制, 程序能夠經過跟蹤對象的引用計數信息, 在適當的時候自動釋放對象並進行內存回收。
  • 每一個對象的引用計數信息由 redisObject 結構的 refcount 屬性記錄:
     1 typedef struct redisObject {
     2 
     3     // ...
     4 
     5     // 引用計數
     6     int refcount;
     7 
     8     // ...
     9 
    10 } robj;
  • 對象的引用計數信息會隨着對象的使用狀態而不斷變化:
    • 在建立一個新對象時, 引用計數的值會被初始化爲 1 ;
    • 當對象被一個新程序使用時, 它的引用計數值會被增一;
    • 當對象再也不被一個程序使用時, 它的引用計數值會被減一;
    • 當對象的引用計數值變爲 0 時, 對象所佔用的內存會被釋放。
  • 表 8-12 列出了修改對象引用計數的 API , 這些 API 分別用於增長、減小、重置對象的引用計數。
函數 做用
incrRefCount 將對象的引用計數值增一。
decrRefCount 將對象的引用計數值減一, 當對象的引用計數值等於 0 時, 釋放對象。
resetRefCount 將對象的引用計數值設置爲 0 , 但並不釋放對象, 這個函數一般在須要從新設置對象的引用計數值時使用。
  • 對象的整個生命週期能夠劃分爲建立對象、操做對象、釋放對象三個階段。

做爲例子, 如下代碼展現了一個字符串對象從建立到釋放的整個過程:

1 // 建立一個字符串對象 s ,對象的引用計數爲 1
2 robj *s = createStringObject(...)
3 
4 // 對象 s 執行各類操做 ...
5 
6 // 將對象 s 的引用計數減一,使得對象的引用計數變爲 0
7 // 致使對象 s 被釋放
8 decrRefCount(s)

其餘不一樣類型的對象也會經歷相似的過程。

 

對象共享

  • 除了用於實現內存回收機制以外, 對象的引用計數屬性還帶有對象共享的做用。
  • 在 Redis 中, 讓多個鍵共享同一個值對象須要執行如下兩個步驟:
    1. 將數據庫鍵的值指針指向一個現有的值對象;
    2. 將被共享的值對象的引用計數增一。

舉個例子, 圖 8-21 就展現了包含整數值 100 的字符串對象同時被鍵 A 和鍵 B 共享以後的樣子, 能夠看到, 除了對象的引用計數從以前的 1 變成了 2 以外, 其餘屬性都沒有變化。

 

 

  • 共享對象機制對於節約內存很是有幫助, 數據庫中保存的相同值對象越多, 對象共享機制就能節約越多的內存。

好比說, 假設數據庫中保存了整數值 100 的鍵不僅有鍵 A 和鍵 B 兩個, 而是有一百個, 那麼服務器只須要用一個字符串對象的內存就能夠保存本來須要使用一百個字符串對象的內存才能保存的數據。

  • 目前來講, Redis 會在初始化服務器時, 建立一萬個字符串對象, 這些對象包含了從 0 到 9999 的全部整數值, 當服務器須要用到值爲 0 到 9999 的字符串對象時, 服務器就會使用這些共享對象, 而不是新建立對象。

注意

建立共享字符串對象的數量能夠經過修改 redis.h/REDIS_SHARED_INTEGERS 常量來修改。

舉個例子, 若是咱們建立一個值爲 100 的鍵 A , 並使用 OBJECT REFCOUNT 命令查看鍵 A 的值對象的引用計數, 咱們會發現值對象的引用計數爲 2 :

1 redis> SET A 100
2 OK
3 
4 redis> OBJECT REFCOUNT A
5 (integer) 2

 

引用這個值對象的兩個程序分別是持有這個值對象的服務器程序, 以及共享這個值對象的鍵 A , 如圖 8-22 所示。

 

 

  • 另外, 這些共享對象不僅僅只有字符串鍵可使用, 那些在數據結構中嵌套了字符串對象的對象(linkedlist 編碼的列表對象、 hashtable 編碼的哈希對象、 hashtable 編碼的集合對象、以及 zset 編碼的有序集合對象)均可以使用這些共享對象。

 

爲何 Redis 不共享包含字符串的對象?

當服務器考慮將一個共享對象設置爲鍵的值對象時, 程序須要先檢查給定的共享對象和鍵想建立的目標對象是否徹底相同, 只有在共享對象和目標對象徹底相同的狀況下, 程序纔會將共享對象用做鍵的值對象, 而一個共享對象保存的值越複雜, 驗證共享對象和目標對象是否相同所需的複雜度就會越高, 消耗的 CPU 時間也會越多:

  • 若是共享對象是保存整數值的字符串對象, 那麼驗證操做的複雜度爲 O(1) ;
  • 若是共享對象是保存字符串值的字符串對象, 那麼驗證操做的複雜度爲 O(N) ;
  • 若是共享對象是包含了多個值(或者對象的)對象, 好比列表對象或者哈希對象, 那麼驗證操做的複雜度將會是 O(N^2) 。

所以, 儘管共享更復雜的對象能夠節約更多的內存, 但受到 CPU 時間的限制, Redis 只對包含整數值的字符串對象進行共享。

 

對象的空轉時長

  • 除了前面介紹過的 type 、 encoding 、 ptr 和 refcount 四個屬性以外, redisObject 結構包含的最後一個屬性爲 lru 屬性, 該屬性記錄了對象最後一次被命令程序訪問的時間:
typedef struct redisObject {
   // ... 
   unsigned lru:22; 
   // ... 
} robj;
  • OBJECT IDLETIME 命令能夠打印出給定鍵的空轉時長, 這一空轉時長就是經過將當前時間減去鍵的值對象的 lru 時間計算得出的.
  • 除了能夠被 OBJECT IDLETIME 命令打印出來以外, 鍵的空轉時長還有另一項做用: 若是服務器打開了 maxmemory 選項, 而且服務器用於回收內存的算法爲 volatile-lru 或者 allkeys-lru , 那麼當服務器佔用的內存數超過了 maxmemory 選項所設置的上限值時, 空轉時長較高的那部分鍵會優先被服務器釋放, 從而回收內存。
    • 配置文件的 maxmemory 選項和 maxmemory-policy 選項的說明介紹了關於這方面的更多信息。
 1 redis> SET msg "hello world"
 2 OK
 3 
 4 # 等待一小段時間
 5 redis> OBJECT IDLETIME msg
 6 (integer) 20
 7 
 8 # 等待一陣子
 9 redis> OBJECT IDLETIME msg
10 (integer) 180
11 
12 # 訪問 msg 鍵的值
13 redis> GET msg
14 "hello world"
15 
16 # 鍵處於活躍狀態,空轉時長爲 0
17 redis> OBJECT IDLETIME msg
18 (integer) 0

 

Redis五種類型的鍵的介紹到這裏就結束了,歡迎和你們討論、交流。 

 

內容參考自: 《Redis設計與實現》

 

 ========== 碼字不易,轉載請註明出處 ==========

相關文章
相關標籤/搜索