簡單動態字符串
鏈表
字典
跳躍表
整數集合
壓縮列表
對象 node
Redis 沒有直接使用 C 語言傳統的字符串表示(以空字符結尾的字符數組,如下簡稱 C 字符串), 而是本身構建了一種名爲簡單動態字符串(simple dynamic string,SDS)的抽象類型, 並將 SDS 用做 Redis 的默認字符串表示。數據庫
每一個 sds.h/sdshdr 結構表示一個 SDS 值:數組
struct sdshdr { // 記錄 buf 數組中已使用字節的數量 // 等於 SDS 所保存字符串的長度 int len; // 記錄 buf 數組中未使用字節的數量 int free; // 字節數組,用於保存字符串 char buf[]; };
表 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 實現了空間預分配和惰性空間釋放兩種優化策略。服務器
空間預分配用於優化 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 。
惰性空間釋放用於優化 SDS 的字符串縮短操做: 當 SDS 的 API 須要縮短 SDS 保存的字符串時, 程序並不當即使用內存重分配來回收縮短後多出來的字節, 而是使用 free 屬性將這些字節的數量記錄起來, 並等待未來使用。
與此同時, SDS 也提供了相應的 API , 讓咱們能夠在有須要時, 真正地釋放 SDS 裏面的未使用空間, 因此不用擔憂惰性空間釋放策略會形成內存浪費。
雖然 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 構建了本身的鏈表實現。
每一個鏈表節點使用一個 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 成員則是用於實現多態鏈表所需的類型特定函數:
圖 3-2 是由一個 list 結構和三個 listNode 結構組成的鏈表:
函數 | 做用 | 時間複雜度 |
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 構建了本身的字典實現。
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;
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 結構保存着一個鍵值對。
圖 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;
圖 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)的問題,它指向另外一個哈希表節點的指針。
由於 dictEntry 節點組成的鏈表沒有指向鏈表表尾的指針, 因此爲了速度考慮, 程序老是將新節點添加到鏈表的表頭位置(複雜度爲 O(1)), 排在其餘已有節點的前面。
隨着操做的不斷執行, 哈希表保存的鍵值對會逐漸地增多或者減小, 爲了讓哈希表的負載因子(load factor)維持在一個合理的範圍以內, 當哈希表保存的鍵值對數量太多或者太少時, 程序須要對哈希表的大小進行相應的擴展或者收縮。
擴展和收縮哈希表的工做能夠經過執行 rehash (從新散列)操做來完成, Redis 對字典的哈希表執行 rehash 的步驟以下:
舉個例子, 假設程序要對含有5個鍵值對字典的 ht[0] 進行擴展操做, 那麼程序將執行如下步驟:
其中哈希表的負載因子能夠經過公式計算得出:
1 # 負載因子 = 哈希表已保存節點數量 / 哈希表大小 2 load_factor = ht[0].used / ht[0].size
根據 BGSAVE 命令或 BGREWRITEAOF 命令是否正在執行, 服務器執行擴展操做所需的負載因子並不相同, 這是由於在執行 BGSAVE 命令或 BGREWRITEAOF 命令的過程當中, Redis 須要建立當前服務器進程的子進程, 而大多數操做系統都採用寫時複製(copy-on-write)技術來優化子進程的使用效率, 因此在子進程存在期間, 服務器會提升執行擴展操做所需的負載因子, 從而儘量地避免在子進程存在期間進行哈希表擴展操做, 這能夠避免沒必要要的內存寫入操做, 最大限度地節約內存。
哈希表漸進式 rehash 的詳細步驟:
問題:若是漸進式rehash過程當中,鍵值對數量迅速增大,最終在尚未rehash完,又須要擴容狀況怎麼辦?
表 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.h/zskiplistNode 和 redis.h/zskiplist 兩個結構定義, 其中 zskiplistNode 結構用於表示跳躍表節點, 而 zskiplist 結構則用於保存跳躍表節點的相關信息, 好比節點的數量, 以及指向表頭節點和表尾節點的指針, 等等。
圖 5-1 展現了一個跳躍表示例, 位於圖片最左邊的是 zskiplist 結構, 該結構包含如下屬性:
位於 zskiplist 結構右方的是四個 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;
圖 5-2 分別展現了三個高度爲 1 層、 3 層和 5 層的節點, 由於 C 語言的數組索引老是從 0 開始的, 因此節點的第一層是 level[0] , 而第二層是 level[1] , 以此類推。
每一個層都有一個指向表尾方向的前進指針(level[i].forward 屬性), 用於從表頭向表尾方向訪問節點。
圖 5-3 用虛線表示出了程序從表頭向表尾方向, 遍歷跳躍表中全部節點的路徑:
舉個例子, 圖 5-4 用虛線標記了在跳躍表中查找分值爲 3.0 、 成員對象爲 o3 的節點時, 沿途經歷的層: 查找的過程只通過了一個層, 而且層的跨度爲 3 , 因此目標節點在跳躍表中的排位爲 3 。
再舉個例子, 圖 5-5 用虛線標記了在跳躍表中查找分值爲 2.0 、 成員對象爲 o2 的節點時, 沿途經歷的層: 在查找節點的過程當中, 程序通過了兩個跨度爲 1 的節點, 所以能夠計算出, 目標節點在跳躍表中的排位爲 2 。
圖 5-6 用虛線展現了若是從表尾向表頭遍歷跳躍表中的全部節點: 程序首先經過跳躍表的 tail 指針訪問表尾節點, 而後經過後退指針訪問倒數第二個節點, 以後再沿着後退指針訪問倒數第三個節點, 再以後遇到指向 NULL 的後退指針, 因而訪問結束。
舉個例子, 在圖 5-7 所示的跳躍表中, 三個跳躍表節點都保存了相同的分值 10086.0 , 但保存成員對象 o1 的節點卻排在保存成員對象 o2 和 o3 的節點以前, 而保存成員對象 o2 的節點又排在保存成員對象 o3 的節點以前, 因而可知, o1 、 o2 、 o3 三個成員對象在字典中的排序爲 o1 <= o2 <= o3 。
雖然僅靠多個跳躍表節點就能夠組成一個跳躍表, 但經過使用一個 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) 。
整數集合(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), 各個項在數組中按值的大小從小到大有序地排列, 而且數組中不包含任何重複項。
雖然 intset 結構將 contents 屬性聲明爲 int8_t 類型的數組, 但實際上 contents 數組並不保存任何 int8_t 類型的值 —— contents 數組的真正類型取決於 encoding 屬性的值:
每當咱們要將一個新元素添加到整數集合裏面, 而且新元素的類型比整數集合現有全部元素的類型都要長時, 整數集合須要先進行升級(upgrade), 而後才能將新元素添加到整數集合裏面。
升級整數集合並添加新元素共分爲三步進行:
由於每次向整數集合添加新元素均可能會引發升級, 而每次升級都須要對底層數組中已有的全部元素進行類型轉換, 因此向整數集合添加新元素的時間複雜度爲 O(N) 。
由於引起升級的新元素的長度老是比整數集合現有全部元素的長度都大, 因此這個新元素的值要麼就大於全部現有元素, 要麼就小於全部現有元素:
整數集合的升級策略有兩個好處, 一個是提高整數集合的靈活性, 另外一個是儘量地節約內存。
表 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) |
圖 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 ),用於標記壓縮列表的末端。 |
圖 7-5 展現了一個包含一字節長 previous_entry_length 屬性的壓縮列表節點, 屬性的值爲 0x05 , 表示前一節點的長度爲 5 字節。
圖 7-6 展現了一個包含五字節長 previous_entry_length 屬性的壓縮節點, 屬性的值爲 0xFE00002766 , 其中值的最高位字節 0xFE 表示這是一個五字節長的 previous_entry_length 屬性, 而以後的四字節 0x00002766 (十進制值 10086 )纔是前一節點的實際長度。
節點的 encoding 屬性記錄了節點的 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 屬性。 |
由於以上緣由, ziplistPush 等命令的平均複雜度僅爲 O(N) , 在實際中, 咱們能夠放心地使用這些函數, 而沒必要擔憂連鎖更新會影響壓縮列表的性能。
表 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)、雙端鏈表、字典、壓縮列表、整數集合, 等等。
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
表 8-1 對象的類型
類型常量 | 對象的名稱 |
REDIS_STRING | 字符串對象 |
REDIS_LIST | 列表對象 |
REDIS_HASH | 哈希對象 |
REDIS_SET | 集合對象 |
REDIS_ZSET | 有序集合對象 |
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" |
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" |
類型常量 | 編碼 | 對象 |
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"
舉個例子, 在列表對象包含的元素比較少時, Redis 使用壓縮列表做爲列表對象的底層實現:
其餘類型的對象也會經過使用多種不一樣的編碼來進行相似的優化。
在接下來的內容中, 咱們將分別介紹 Redis 中的五種不一樣類型的對象, 說明這些對象底層所使用的編碼方式, 列出對象從一種編碼轉換成另外一種編碼所需的條件, 以及同一個命令在多種不一樣編碼上的實現方法。
舉個例子, 若是咱們執行如下 SET 命令, 那麼服務器將建立一個如圖 8-1 所示的 int 編碼的字符串對象做爲 number 鍵的值:
1 redis> SET number 10086 2 OK 3 4 redis> OBJECT ENCODING number 5 "int"
舉個例子, 若是咱們執行如下命令, 那麼服務器將建立一個如圖 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"
embstr 編碼是專門用於保存短字符串的一種優化編碼方式, 這種編碼和 raw 編碼同樣, 都使用 redisObject 結構和 sdshdr 結構來表示字符串對象, 但 raw 編碼會調用兩次內存分配函數來分別建立 redisObject 結構和 sdshdr 結構, 而 embstr 編碼則經過調用一次內存分配函數來分配一塊連續的空間, 空間中依次包含 redisObject 和 sdshdr 兩個結構, 如圖 8-3 所示。
embstr 編碼的字符串對象在執行命令時, 產生的效果和 raw 編碼的字符串對象執行命令時產生的效果是相同的, 但使用 embstr 編碼的字符串對象來保存短字符串值有如下好處:
做爲例子, 如下命令建立了一個 embstr 編碼的字符串對象做爲 msg 鍵的值, 值對象的樣子如圖 8-4 所示:
1 redis> SET msg "hello" 2 OK 3 4 redis> OBJECT ENCODING msg 5 "embstr"
表 8-6 字符串對象保存各種型值的編碼方式
值 | 編碼 |
能夠用 long 類型保存的整數。 | int |
能夠用 long double 類型保存的浮點數。 | embstr 或者 raw |
字符串值, 或者由於長度太大而沒辦法用 long 類型表示的整數, 又或者由於長度太大而沒辦法用 long double 類型表示的浮點數。 | 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 | 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 而後取出並返回字符串指定索引上的字符。 | 直接取出並返回字符串指定索引上的字符。 |
舉個例子, 若是咱們執行如下 RPUSH 命令, 那麼服務器將建立一個列表對象做爲 numbers 鍵的值:
1 redis> RPUSH numbers 1 "three" 5 2 (integer) 3
注意, linkedlist 編碼的列表對象在底層的雙端鏈表結構中包含了多個字符串對象, 這種嵌套字符串對象的行爲在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現, 字符串對象是 Redis 五種類型的對象中惟一一種會被其餘四種類型對象嵌套的對象。
注意
爲了簡化字符串對象的表示, 咱們在圖 8-6 使用了一個帶有 StringObject 字樣的格子來表示一個字符串對象, 而 StringObject 字樣下面的是字符串對象所保存的值。
好比說, 圖 8-7 表明的就是一個包含了字符串值 "three" 的字符串對象, 它是 8-8 的簡化表示。
本書接下來的內容將繼續沿用這一簡化表示。
當列表對象能夠同時知足如下兩個條件時, 列表對象使用 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 函數, 定位到雙端鏈表指定索引上的節點, 而後經過賦值操做更新節點的值。 |
舉個例子, 若是咱們執行如下 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 編碼:
不能知足這兩個條件的哈希對象須要使用 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 函數返回字典的值。 |
舉個例子, 如下代碼將建立一個如圖 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 編碼:
不能知足這兩個條件的集合對象須要使用 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 函數, 從字典中刪除全部鍵爲給定元素的鍵值對。 |
1 typedef struct zset { 2 3 zskiplist *zsl; 4 dict *dict; 5 6 } zset;
舉個例子, 若是咱們執行如下 ZADD 命令, 那麼服務器將建立一個有序集合對象做爲 price 鍵的值:
1 redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry 2 (integer) 3
注意
爲了展現方便, 圖 8-17 在字典和跳躍表中重複展現了各個元素的成員和分值, 但在實際中, 字典和跳躍表會共享元素的成員和分值, 因此並不會形成任何數據重複, 也不會所以而浪費任何內存。
當有序集合對象能夠同時知足如下兩個條件時, 對象使用 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 | 遍歷壓縮列表, 查找包含了給定成員的節點, 而後取出成員節點旁邊的分值節點保存的元素分值。 | 直接從字典中取出給定成員的分值。 |
例子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 屬性來實現的:
舉個例子, 對於 LLEN 命令來講:
其餘類型特定命令的類型檢查過程也和這裏展現的 LLEN 命令的類型檢查過程相似。
如今, 考慮這樣一個狀況, 若是咱們對一個鍵執行 LLEN 命令, 那麼服務器除了要確保執行命令的是列表鍵以外, 還須要根據鍵的值對象所使用的編碼來選擇正確的 LLEN 命令實現:
借用面向對象方面的術語來講, 咱們能夠認爲 LLEN 命令是多態(polymorphism)的: 只要執行 LLEN 命令的是列表鍵, 那麼不管值對象使用的是 ziplist 編碼仍是 linkedlist 編碼, 命令均可以正常執行。
圖 8-19 其餘類型特定命令的執行過程也是相似的。
實際上, 咱們能夠將 DEL 、 EXPIRE 、 TYPE 等命令也稱爲多態命令, 由於不管輸入的鍵是什麼類型, 這些命令均可以正確地執行。他們和 LLEN 等命令的區別在於, 前者是基於類型的多態 —— 一個命令能夠同時用於處理多種不一樣類型的鍵, 而後者是基於編碼的多態 —— 一個命令能夠同時用於處理多種不一樣編碼。
1 typedef struct redisObject { 2 3 // ... 4 5 // 引用計數 6 int refcount; 7 8 // ... 9 10 } robj;
函數 | 做用 |
incrRefCount | 將對象的引用計數值增一。 |
decrRefCount | 將對象的引用計數值減一, 當對象的引用計數值等於 0 時, 釋放對象。 |
resetRefCount | 將對象的引用計數值設置爲 0 , 但並不釋放對象, 這個函數一般在須要從新設置對象的引用計數值時使用。 |
做爲例子, 如下代碼展現了一個字符串對象從建立到釋放的整個過程:
1 // 建立一個字符串對象 s ,對象的引用計數爲 1 2 robj *s = createStringObject(...) 3 4 // 對象 s 執行各類操做 ... 5 6 // 將對象 s 的引用計數減一,使得對象的引用計數變爲 0 7 // 致使對象 s 被釋放 8 decrRefCount(s)
其餘不一樣類型的對象也會經歷相似的過程。
舉個例子, 圖 8-21 就展現了包含整數值 100 的字符串對象同時被鍵 A 和鍵 B 共享以後的樣子, 能夠看到, 除了對象的引用計數從以前的 1 變成了 2 以外, 其餘屬性都沒有變化。
好比說, 假設數據庫中保存了整數值 100 的鍵不僅有鍵 A 和鍵 B 兩個, 而是有一百個, 那麼服務器只須要用一個字符串對象的內存就能夠保存本來須要使用一百個字符串對象的內存才能保存的數據。
注意
建立共享字符串對象的數量能夠經過修改 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 所示。
當服務器考慮將一個共享對象設置爲鍵的值對象時, 程序須要先檢查給定的共享對象和鍵想建立的目標對象是否徹底相同, 只有在共享對象和目標對象徹底相同的狀況下, 程序纔會將共享對象用做鍵的值對象, 而一個共享對象保存的值越複雜, 驗證共享對象和目標對象是否相同所需的複雜度就會越高, 消耗的 CPU 時間也會越多:
所以, 儘管共享更復雜的對象能夠節約更多的內存, 但受到 CPU 時間的限制, Redis 只對包含整數值的字符串對象進行共享。
typedef struct redisObject { // ... unsigned lru:22; // ... } robj;
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設計與實現》
========== 碼字不易,轉載請註明出處 ==========