redis中的很多操作都是基於上面介紹的redis對象,瞭解這些對象的底層實現,可以為之後更多的redis特性做準備。 ...
結構定義
在redis中,對象的數據結構定義如下:
typedef struct redisObject {
unsigned type:4;
unsgined encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
}
結構定義中的type:4
、encoding:4
這種定義方式稱為位段類型。
使用位段類型的好處就是避免浪費記憶體,如果使用
unsigned int type
定義type欄位,需要4個位元組,而使用unsigned type:4
,只需要4個位段就足夠了。
參數說明
redis對象有許多特性,比如:類型檢查(通過type實現)、命令多態(encoding實現)、記憶體共用(通過refcount實現)等等,這些特性都是通過redisObject中的參數實現的。
type
對象類型,它的取值範圍有五個,分別是redis使用的五種對象類型:
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4
在執行命令前對type欄位進行檢查,可判斷出對象是否是命令允許執行的對象類型。
encoding
對象使用的編碼類型,它的取值範圍有下麵這些:
#define OBJ_ENCODING_RAW 0 /* 動態字元串 */
#define OBJ_ENCODING_INT 1 /* 整數 */
#define OBJ_ENCODING_HT 2 /* 哈希表 */
#define OBJ_ENCODING_ZIPMAP 3
#define OBJ_ENCODING_LINKEDLIST 4 /* 舊的列表編碼,現在不再使用了 */
#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 /* 壓縮鏈表和雙向鏈表組成的快速列表 */
在調用命令的時候,redis還會根據對象使用的編碼類型來選擇正確的底層對象,執行對應函數的實現代碼。
lru
最近最後一次被命令訪問的時間 或者 最近最少使用的數據。
在執行OBJECT IDLETIME命令時,通過當前時間減去lru屬性的值,得到鍵的空轉時長。另外,如果伺服器打開了maxmemory選項,且使用的記憶體回收演算法是volatile-lur或者allkeys-lru,那麼當伺服器占用的記憶體超過了maxmemory的上限值,空轉時長較高的鍵會優先被伺服器釋放,從而回收記憶體。
refcount
對象的引用計數。
redis的對象共用和記憶體回收特性就是通過refcount屬性來實現,通過將refcount + 1實現對象共用;進行記憶體回收檢查時,檢查refcount == 0的對象,將對象進行回收。
ptr
指向底層數據結構用於保存數據的指針。
對象使用的數據結構
redis有五種對象,不同對象可能用到的數據結構如下圖所示:
編碼轉換與命令多態
同一種對象使用不同的數據結構是通過encoding來實現,而且,同一個命令的實現方法會根據對象的編碼屬性而變化,這是命令的多態實現。
以哈希對象為例看看編碼轉換以及命令多態等特性是怎麼實現的。
哈希對象
哈希對象使用的編碼有:ziplist、hashtable。
如果使用壓縮表作為底層實現,每當有新的鍵值對需要加入哈希對象,會先添加鍵節點到鏈表,然後添加值節點。
使用hashtable作為底層實現,每一個新的鍵值對都會使用字典鍵值對來保存,鍵和值分別是字元串對象。
使用不同結構保存後的結構圖如下所示:
ziplist編碼
hashtable編碼
編碼轉換
每一種對象在使用編碼的時候都有一定的條件,使用ziplist編碼的哈希對象都應該滿足兩個條件:
- 1、所有鍵值對的鍵和值字元串對象長度小於64位元組
- 2、哈希對象保存的鍵值對數量小於512個
如果不能滿足上述條件時,redis會進行對哈希對象底層數據結構進行從壓縮表到字典的轉換,實現步驟是遍歷壓縮表,獲取壓縮表中的鍵和值,使用得到的鍵和值創建一個字典對象,然後添加字典里,具體代碼如下:
hashTypeIterator *hi;
dict *dict;
int ret;
// 創建遍歷器對象和哈希表
hi = hashTypeInitIterator(o);
dict = dictCreate(&hashDictType, NULL);
while (hashTypeNext(hi) != C_ERR) {
sds key, value;
// 用獲取ziplis中的key、value新增鍵值對到哈希表
key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
ret = dictAdd(dict, key, value);
if (ret != DICT_OK) {
serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
o->ptr,ziplistBlobLen(o->ptr));
serverPanic("Ziplist corruption detected");
}
}
hashTypeReleaseIterator(hi);
zfree(o->ptr);
o->encoding = OBJ_ENCODING_HT;
o->ptr = dict;
命令多態
命令多態是檢查對象的編碼,然後執行不同的實現方式。比如哈希對象中的hget命令。
hget命令實現代碼:
void hgetCommand(client *c) {
robj *o;
// key不存在,返回空
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
addHashFieldToReply(c, o, c->argv[2]->ptr);
}
hget命令的實現最終是調用addHashFieldToReply函數(代碼如下),該函數是通過判斷哈希對象的編碼來決定使用什麼函數來獲取哈希對象具體field的值,其他命令的實現也是大同小異。
static void addHashFieldToReply(client *c, robj *o, sds field) {
int ret;
if (o == NULL) {
addReply(c, shared.nullbulk);
return;
}
// 根據底層不同編碼獲取field的值
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX;
ret = hashTypeGetFromZiplist(o, field, &vstr, &vlen, &vll);
if (ret < 0) {
addReply(c, shared.nullbulk);
} else {
if (vstr) {
addReplyBulkCBuffer(c, vstr, vlen);
} else {
addReplyBulkLongLong(c, vll);
}
}
} else if (o->encoding == OBJ_ENCODING_HT) {
sds value = hashTypeGetFromHashTable(o, field);
if (value == NULL)
addReply(c, shared.nullbulk);
else
addReplyBulkCBuffer(c, value, sdslen(value));
} else {
serverPanic("Unknown hash encoding");
}
}
總結
redis中的很多操作都是基於上面介紹的redis對象,瞭解這些對象的底層實現,可以為之後更多的redis特性做準備。