目前為止,我們介紹了 redis 中非常典型的五種數據結構,從 SDS 到 壓縮列表,這都是 redis 最底層、最常用的數據結構,相信你也掌握的不錯。
但 redis 實際存儲鍵值對的時候,是基於對象這個基本單位的,並且往往一個對象下面對對應不同的底層數據結構實現以便於在不同的場景下切換底層實現提升效率。例如列表對象在元素不多情況話會使用壓縮列表來實現以壓縮內存,而在元素比較多的時候常規的雙端鏈表進行實現。
下面我們就具體來看看 redis 中都有哪些對象,底層又對應哪些可供選擇的數據結構。
一、Redis 對象結構定義
redis 為每個對象定義為如下數據結構:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
type 記錄的是當前的對象類型,有以下幾種類型:
#define OBJ_STRING 0 /*字符串對象*/
#define OBJ_LIST 1 /*列表對象*/
#define OBJ_SET 2 /*集合對象*/
#define OBJ_ZSET 3 /*有序集合對象*/
#define OBJ_HASH 4 /*哈希對象*/
encoding 記錄的是當前對象使用的哪種底層數據結構實現的,有以下類型可供選擇:
#define OBJ_ENCODING_RAW 0 /* SDS 字符串 */
#define OBJ_ENCODING_INT 1 /* 整數 */
#define OBJ_ENCODING_HT 2 /* 字典結構 */
#define OBJ_ENCODING_ZIPMAP 3 /* 壓縮map,已經廢棄 */
#define OBJ_ENCODING_LINKEDLIST 4 /* LinkedList 雙端鏈表,廢棄了 */
#define OBJ_ENCODING_ZIPLIST 5 /* 壓縮列表 */
#define OBJ_ENCODING_INTSET 6 /* 整數集合 */
#define OBJ_ENCODING_SKIPLIST 7 /* 跳躍表 */
#define OBJ_ENCODING_EMBSTR 8 /* 短字符串 */
#define OBJ_ENCODING_QUICKLIST 9 /* 壓縮鏈表和雙向鏈表組成的快速列表 */
8 和 9 我們遇到時在介紹,這里暫時不做介紹。
lru 記錄的是上一個當前對象實例被訪問的時間,它用作計算對象空轉時長,空轉時長過大的對象會被 redis 優先釋放內存。
refcount 記錄的是對象的引用計數,引用計數算法是很多編程語言中管理對象是否應該被銷毀的依據,和它類似的典型的 Java 中可達性分析算法,都是用於計數當前對象是否依然被使用,以便釋放內存。
ptr 指針指向的是實際實現當前對象的數據結構首地址。
以上就是 redisObject 數據結構的基本解釋,下面我們看具體的對象分別會在什么情況下切換不同的底層實現。
二、字符串對象
字符串對象有三種 encoding 值,也就是只有這三種情況,redis 才會使用字符串對象存儲數據。
- 字符串(raw):普通的字符串
- 整數(int):long 類型的整數值
- 短字符串(embstr ):短字符串
如果判定使用 raw 編碼,那么 redis 的 ptr 指針將會指向一個 SDS 結構,如果確定使用 int 編碼,那么會將 redisObject 中 ptr 類型由 void* 變成 long,繼而分配 robj 內存。
當字符串的長度小於 39 個字節時,會采用 embstr 這種編碼,embstr 其實也是使用 SDS 進行存儲,區別於 raw 編碼的是,后者會將 robj 和 ptr 指向的 SDS 分配在連續的內存塊,唯一的好處是分配和釋放內存都只需要一次操作即可完成,再一個是因為數據相鄰,有可能一次加載 robj 的時候,CPU 將后面的 embstr 也加載進緩存,等到訪問的時候就可以直接從緩存中訪問。
但是,我們看一個例子:
hello 原本是以 int 編碼存儲的,但是我們執行 append 命令添加了字符串之后,它變成了 raw 編碼。
這其實是 redis 的一種編碼換換,當 hello 不再適合使用 int 編碼繼續存儲的時候,會進行一個編碼轉換。
三、列表對象
列表對象有兩種編碼,壓縮列表 ziplist 和 linkedlist。我們之前說過壓縮列表的推薦應用場景,少量整數或字符串的時候可以用壓縮列表來節省內存空間,而大數據量的節點則推薦使用普通的雙端鏈表進行實現。
但是實際上,redis 的較新版本已經使用一種叫 quicklist 的快速列表整合 ziplist 和 linkedlist 作為列表對象的實現了。它將所有的節點分段拆分,每一份又使用壓縮列表進行壓縮,不同段之間使用雙向指針連接。
四、集合對象
集合對象也有兩種編碼,整數集合 intset 和 字典 hashtable。默認情況下,當集合中有且僅有整型數據,且不超過 512 個,那么 redis 會使用整數集合 intset 進行集合存儲,其余情況 redis 則構建字典進行集合數據存儲。
順便給大家復習下 intset 的無重復性、順序性的特性,重復的元素是插入不進去的,因為插入之前會通過二分查找查找是否存在該元素,如果存在則拒絕插入操作。
當然了,如果集合中元素個數超過 512,那么 redis 就轉而使用字典結構進行數據存儲,具體實例就不再演示了。
五、有序集合對象
有序集合對象同樣使用兩種編碼 ziplist 和 skiplist,可能你又見到壓縮列表的身影了,足以見得,壓縮列表是一個非常優秀的數據結構。
同樣,當有序集合中包含少量元素的時候,redis 會優先使用壓縮列表進行存儲,反之選擇跳躍表。
sadd 命令的標准語法是:
ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN
每一個元素都會對應一個分值,skiplist 本身的實現就需要這個分值進行元素的存儲排序,有的時候有序集合會使用壓縮列表進行實現,那么也需要這個分值來有序的壓縮元素,這也是壓縮列表頁可以實現有序集合的原因。
這里補充一下,雖然說 redis 的有序集合是跳表實現的,這句話不錯,但有失偏駁。
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
准確來說,redis 中的有序集合是由我們之前介紹過的字典加上跳表(組合起來就是zset)實現的,字典中保存的數據和分數 score 的映射關系,每次插入數據會從字典中查詢,如果已經存在了,就不再插入,有序集合中是不允許重復數據。
六、哈希對象
哈希對象的編碼可以是 ziplist 或者 hashtable,沒什么特殊的,不再贅述。
以上,我們總結了 redis 中五大對象結構,以及他們可選的底層實現數據結構,相信你也理解的不錯,這將非常有助於我們后面的學習。
下節開始,我們向 redis 數據庫邁進~