在 Redis 的命令中,用于對鍵(key)進(jìn)行處理的命令占了很大一部分,而對于鍵所保存的值的類型(后簡稱“鍵的類型”),鍵能執(zhí)行的命令又各不相同。
比如說,LPUSH 和 LLEN 只能用于列表鍵,而 SADD 和 SRANDMEMBER 只能用于集合鍵,等等。
另外一些命令,比如 DEL 、 TTL 和 TYPE ,可以用于任何類型的鍵,但是,要正確實(shí)現(xiàn)這些命令,必須為不同類型的鍵設(shè)置不同的處理方式:比如說,刪除一個(gè)列表鍵和刪除一個(gè)字符串鍵的操作過程就不太一樣。
以上的描述說明,Redis 必須讓每個(gè)鍵都帶有類型信息,使得程序可以檢查鍵的類型,并為它選擇合適的處理方式。
另外,在前面介紹各個(gè)底層數(shù)據(jù)結(jié)構(gòu)時(shí)有提到,Redis 的每一種數(shù)據(jù)類型,比如字符串、列表、有序集,它們都擁有不只一種底層實(shí)現(xiàn)(Redis 內(nèi)部稱之為編碼,encoding),這說明,每當(dāng)對某種數(shù)據(jù)類型的鍵進(jìn)行操作時(shí),程序都必須根據(jù)鍵所采取的編碼,進(jìn)行不同的操作。
比如說,集合類型就可以由字典和整數(shù)集合兩種不同的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),但是,當(dāng)用戶執(zhí)行 ZADD 命令時(shí),他/她應(yīng)該不必關(guān)心集合使用的是什么編碼,只要 Redis 能按照 ZADD 命令的指示,將新元素添加到集合就可以了。
這說明,操作數(shù)據(jù)類型的命令除了要對鍵的類型進(jìn)行檢查之外,還需要根據(jù)數(shù)據(jù)類型的不同編碼進(jìn)行多態(tài)處理。
為了解決以上問題,Redis 構(gòu)建了自己的類型系統(tǒng),這個(gè)系統(tǒng)的主要功能包括:
redisObject
對象。redisObject
對象的類型檢查。redisObject
對象的顯式多態(tài)函數(shù)。redisObject
進(jìn)行分配、共享和銷毀的機(jī)制。以下小節(jié)將分別介紹類型系統(tǒng)的這幾個(gè)方面。
Note
因?yàn)?C 并不是面向?qū)ο笳Z言,這里將 redisObject
稱呼為對象一是為了講述的方便,二是希望通過模仿 OOP 的常用術(shù)語,讓這里的內(nèi)容更容易被理解,redisObject
實(shí)際上是只是一個(gè)結(jié)構(gòu)類型。
redisObject
是 Redis 類型系統(tǒng)的核心,數(shù)據(jù)庫中的每個(gè)鍵、值,以及 Redis 本身處理的參數(shù),都表示為這種數(shù)據(jù)類型。
redisObject
的定義位于 redis.h
:
/*
* Redis 對象
*/
typedef struct redisObject {
// 類型
unsigned type:4;
// 對齊位
unsigned notused:2;
// 編碼方式
unsigned encoding:4;
// LRU 時(shí)間(相對于 server.lruclock)
unsigned lru:22;
// 引用計(jì)數(shù)
int refcount;
// 指向?qū)ο蟮闹? void *ptr;
} robj;
type
、 encoding
和 ptr
是最重要的三個(gè)屬性。
type
記錄了對象所保存的值的類型,它的值可能是以下常量的其中一個(gè)(定義位于 redis.h
):
/*
* 對象類型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
encoding
記錄了對象所保存的值的編碼,它的值可能是以下常量的其中一個(gè)(定義位于 redis.h
):
/*
* 對象編碼
*/
#define REDIS_ENCODING_RAW 0 // 編碼為字符串
#define REDIS_ENCODING_INT 1 // 編碼為整數(shù)
#define REDIS_ENCODING_HT 2 // 編碼為哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 編碼為 zipmap
#define REDIS_ENCODING_LINKEDLIST 4 // 編碼為雙端鏈表
#define REDIS_ENCODING_ZIPLIST 5 // 編碼為壓縮列表
#define REDIS_ENCODING_INTSET 6 // 編碼為整數(shù)集合
#define REDIS_ENCODING_SKIPLIST 7 // 編碼為跳躍表
ptr
是一個(gè)指針,指向?qū)嶋H保存值的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)由 type
屬性和 encoding
屬性決定。
舉個(gè)例子,如果一個(gè) redisObject
的 type
屬性為 REDIS_LIST
, encoding
屬性為 REDIS_ENCODING_LINKEDLIST
,那么這個(gè)對象就是一個(gè) Redis 列表,它的值保存在一個(gè)雙端鏈表內(nèi),而 ptr
指針就指向這個(gè)雙端鏈表;
另一方面,如果一個(gè) redisObject
的 type
屬性為 REDIS_HASH
, encoding
屬性為 REDIS_ENCODING_ZIPMAP
,那么這個(gè)對象就是一個(gè) Redis 哈希表,它的值保存在一個(gè) zipmap
里,而 ptr
指針就指向這個(gè) zipmap
;諸如此類。
下圖展示了 redisObject
、Redis 所有數(shù)據(jù)類型、以及 Redis 所有編碼方式(底層實(shí)現(xiàn))三者之間的關(guān)系:
REDIS_STRING; redisObject -> REDIS_LIST; redisObject -> REDIS_SET; redisObject -> REDIS_ZSET; redisObject -> REDIS_HASH; REDIS_STRING -> REDIS_ENCODING_RAW; REDIS_STRING -> REDIS_ENCODING_INT; REDIS_LIST -> REDIS_ENCODING_LINKEDLIST; REDIS_LIST -> REDIS_ENCODING_ZIPLIST; REDIS_SET -> REDIS_ENCODING_HT; REDIS_SET -> REDIS_ENCODING_INTSET; REDIS_ZSET -> REDIS_ENCODING_SKIPLIST; REDIS_ZSET -> REDIS_ENCODING_ZIPLIST; REDIS_HASH -> REDIS_ENCODING_HT; REDIS_HASH -> REDIS_ENCODING_ZIPLIST;}" />
這個(gè)圖展示了 Redis 各種數(shù)據(jù)類型,以及它們的編碼方式。
Note
REDIS_ENCODING_ZIPMAP
沒有出現(xiàn)在圖中,因?yàn)閺?Redis 2.6 開始,它不再是任何數(shù)據(jù)類型的底層結(jié)構(gòu)。
有了 redisObject
結(jié)構(gòu)的存在,在執(zhí)行處理數(shù)據(jù)類型的命令時(shí),進(jìn)行類型檢查和對編碼進(jìn)行多態(tài)操作就簡單得多了。
當(dāng)執(zhí)行一個(gè)處理數(shù)據(jù)類型的命令時(shí),Redis 執(zhí)行以下步驟:
key
,在數(shù)據(jù)庫字典中查找和它相對應(yīng)的 redisObject
,如果沒找到,就返回 NULL
。redisObject
的 type
屬性和執(zhí)行命令所需的類型是否相符,如果不相符,返回類型錯(cuò)誤。redisObject
的 encoding
屬性所指定的編碼,選擇合適的操作函數(shù)來處理底層的數(shù)據(jù)結(jié)構(gòu)。作為例子,以下展示了對鍵 key
執(zhí)行 LPOP
命令的完整過程:
get_key_obj_from_db; get_key_obj_from_db -> is_obj_nil_or_not; is_obj_nil_or_not -> return_nil [label="是"]; is_obj_nil_or_not -> is_type_list_or_not [label="否"]; is_type_list_or_not -> call_poly_pop_function [label="是"]; is_type_list_or_not -> return_type_error [label="否"]; call_poly_pop_function -> pop_from_ziplist [label="對象的編碼為\nZIPLIST"]; call_poly_pop_function -> pop_from_linkedlist [label="對象的編碼為\nLINKEDLIST"]; pop_from_ziplist -> return_pop_item; pop_from_linkedlist -> return_pop_item;}" />
有一些對象在 Redis 中非常常見,比如命令的返回值 OK
、 ERROR
、 WRONGTYPE
等字符,另外,一些小范圍的整數(shù),比如個(gè)位、十位、百位的整數(shù)都非常常見。
為了利用這種常見情況,Redis 在內(nèi)部使用了一個(gè) Flyweight 模式 :通過預(yù)分配一些常見的值對象,并在多個(gè)數(shù)據(jù)結(jié)構(gòu)之間共享這些對象,程序避免了重復(fù)分配的麻煩,也節(jié)約了一些 CPU 時(shí)間。
Redis 預(yù)分配的值對象有如下這些:
OK
,執(zhí)行錯(cuò)誤時(shí)返回的 ERROR
,類型錯(cuò)誤時(shí)返回的 WRONGTYPE
,命令入隊(duì)事務(wù)時(shí)返回的 QUEUED
,等等。0
在內(nèi),小于 redis.h/REDIS_SHARED_INTEGERS
的所有整數(shù)(REDIS_SHARED_INTEGERS
的默認(rèn)值為 10000
)因?yàn)槊畹幕貜?fù)值直接返回給客戶端,所以它們的值無須進(jìn)行共享;另一方面,如果某個(gè)命令的輸入值是一個(gè)小于 REDIS_SHARED_INTEGERS
的整數(shù)對象,那么當(dāng)這個(gè)對象要被保存進(jìn)數(shù)據(jù)庫時(shí),Redis 就會(huì)釋放原來的值,并將值的指針指向共享對象。
作為例子,下圖展示了三個(gè)列表,它們都帶有指向共享對象數(shù)組中某個(gè)值對象的指針:
更多建議: