雜湊物件的編碼有兩種:ziplist
、hashtable
。
編碼一:ziplist
ziplist
已經是我們的老朋友了,它一出現,那肯定就是為了節省記憶體啦。那麼雜湊物件是怎麼用 ziplist
儲存的呢?
每次插入鍵值對的時候,在 ziplist
列表末尾,挨著插入 field
和 value
。如下圖:
常見操作
增刪改查都涉及到一塊很類似的程式碼,那就是查詢。
redis 這幾個函式的查詢部分,幾乎都是直接複製貼上。。。可能有改動就有點難維護了。
獲取
先從 ziplist 中拿到 field 的指標,然後向後一個節點就是 value
找
field
的時候,ziplistFind
最後一個引數傳入的是1
,表示查一個節點後,跳過一個節點不查。
因為hash
在ziplist
中的存就是field
value
挨著存的,我們查的是field
,所以要跳過value
。
int hashTypeGetFromZiplist(robj *o, sds field, unsigned char **vstr,
unsigned int *vlen, long long *vll) {
unsigned char *zl, *fptr = NULL, *vptr = NULL;
int ret;
serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
zl = o->ptr;
// 獲取 ziplist 頭指標
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// 再呼叫 `ziplist.c/ziplistFind` 查詢跟 field 相等的節點
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
// 獲取 field 的下個指標,就是 value 啦
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
}
}
if (vptr != NULL) {
// 通過上面獲取到的指標,在 ziplist 中獲取對應的值
ret = ziplistGet(vptr, vstr, vlen, vll);
serverAssert(ret);
return 0;
}
return -1;
}
刪除
刪除其實就是先查詢,後刪除
int hashTypeDelete(robj *o, sds field) {
// 0 表示找不到,1 表示刪除成功
int deleted = 0;
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr;
zl = o->ptr;
// 呼叫 ziplist.c/ziplistIndex 的函式,獲取 ziplist 的頭指標
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// 通過 ziplist.c/ziplistFind 函式去找 field 對應的節點指標
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
// 刪除 field
zl = ziplistDelete(zl,&fptr);
// 刪除 value
zl = ziplistDelete(zl,&fptr);
o->ptr = zl;
deleted = 1;
}
}
}
// ...
return deleted;
}
插入 / 更新
一切盡在註釋中
int hashTypeSet(robj *o, sds field, sds value, int flags) {
// 0 表示是插入操作,1 表示是更新操作
int update = 0;
// 如果是 ziplist 編碼
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
// 呼叫 ziplist.c/ziplistIndex 的函式,獲取 ziplist 的頭指標
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// 找 field 對應的指標
fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
// 如果能找到,說明 field 已存在,是更新操作。
if (fptr != NULL) {
// 獲取 field 下一個節點,也就是值(再次強調,ziplist 中 field 和 value 是挨著放的)
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
update = 1;
// 刪除原來的值
zl = ziplistDelete(zl, &vptr);
// 插入新值
zl = ziplistInsert(zl, vptr, (unsigned char*)value, sdslen(value));
}
}
// 如果找不到 field 對應的節點,update == 0,那這就是一個插入操作
if (!update) {
// 在末尾插入 field 和 value
zl = ziplistPush(zl, (unsigned char*)field, sdslen(field), ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)value, sdslen(value), ZIPLIST_TAIL);
}
o->ptr = zl;
// 判斷長度是否達到閾值,如果達到將進行編碼轉換
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
}
// ...
}
編碼二:hashtable
hashtable
編碼用的是字典 dict
作為底層實現,關於 dict
,具體的前文 Redis 設計與實現 4:字典 dict 已經寫了,包括了 dict 基本操作的原始碼解讀。
其結構就相當複雜啦,再來複習一下,如下圖:
常見操作
獲取
hashtable
編碼本身的思路跟 dict
的基本 api 很契合,所以程式碼比較整潔。獲取值就是直接呼叫 dict.c/dictFind
而已。
前文 Redis 設計與實現 4:字典 dict 已經對 dict
的查詢原始碼分析過,感興趣的讀者可以看看。
sds hashTypeGetFromHashTable(robj *o, sds field) {
dictEntry *de;
serverAssert(o->encoding == OBJ_ENCODING_HT);
// 直接呼叫 dict.c/dictFind 找到 dictEntry 鍵值對
de = dictFind(o->ptr, field);
if (de == NULL) return NULL;
return dictGetVal(de);
}
刪除
直接呼叫 dict.c/dictDelete
函式進行刪除。
前文 Redis 設計與實現 4:字典 dict 已經對 dict
的刪除原始碼分析過,感興趣的讀者可以看看。
int hashTypeDelete(robj *o, sds field) {
// 0 表示找不到,1 表示刪除成功
int deleted = 0;
// ...
if (o->encoding == OBJ_ENCODING_HT) {
if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = 1;
/* Always check if the dictionary needs a resize after a delete. */
if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
}
// ...
return deleted;
}
插入 / 更新
hashtable
的 插入 / 更新
邏輯跟 ziplist
類似。也是先檢視是否存在,如果已存在,則刪除原來的值,再重新設定新值; 如果不存在,則新增一整個鍵值對。
這裡比較有趣的是,對 field
和 value
定義了所有權 flags
,如果擁有所有權,則函式可以直接用來設定field
或者 value
,否則只能重新拷貝一份(sds.c/sdsdup
)。
// 所有權定義
#define HASH_SET_TAKE_FIELD (1<<0)
#define HASH_SET_TAKE_VALUE (1<<1)
#define HASH_SET_COPY 0
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
if (o->encoding == OBJ_ENCODING_HT) {
// 先找 field
dictEntry *de = dictFind(o->ptr,field);
if (de) {
// 如果找到了,那就刪掉舊了,然後設定新的
sdsfree(dictGetVal(de));
if (flags & HASH_SET_TAKE_VALUE) {
// 如果擁有 value 的所有權,那麼可以把 value 直接設定進去
dictGetVal(de) = value;
value = NULL;
} else {
// 如果不擁有 value 的所有權,例如複製的時候。那麼要拷貝一個新的 value 出來
dictGetVal(de) = sdsdup(value);
}
update = 1;
} else {
// 如果找不到值,那麼要新設定值
sds f,v;
// 如果擁有 field 的所有權,那麼直接用於 field,否則需要重新拷貝一份
if (flags & HASH_SET_TAKE_FIELD) {
f = field;
field = NULL;
} else {
f = sdsdup(field);
}
// 同樣,只有擁有 value 的所有權,才能直接用,否則要拷貝一份
if (flags & HASH_SET_TAKE_VALUE) {
v = value;
value = NULL;
} else {
v = sdsdup(value);
}
// 再呼叫 dict.c 的 dictAdd 新增
dictAdd(o->ptr,f,v);
}
}
// ...
}
編碼轉換
當雜湊物件可以同時滿足以下兩個條件時,雜湊物件使用 ziplist
編碼:
- 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於
64
位元組 (可通過配置hash-max-ziplist-value
修改) - 雜湊物件儲存的鍵值對數量小於
512
個 (可通過配置hash-max-ziplist-entries
修改)
不能同時滿足這兩個條件的雜湊物件需要使用 hashtable
編碼。
在 hsetnxCommand
和 hsetCommand
函式中,都會呼叫到編碼的轉換。程式碼如下
void hsetnxCommand(client *c) {
// ...
hashTypeTryConversion(o,c->argv,2,3);
// ...
hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);
// ...
}
void hsetCommand(client *c) {
// ...
hashTypeTryConversion(o,c->argv,2,c->argc-1);
// ...
hashTypeSet(o,c->argv[2]->ptr,c->argv[3]->ptr,HASH_SET_COPY);
// ...
}
// 檢查長度超過 hash_max_ziplist_value 就轉編碼
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
for (i = start; i <= end; i++) {
// #define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
}
}
int hashTypeSet(robj *o, sds field, sds value, int flags) {
// ...
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
// ...
// 判斷長度是否達到閾值,如果達到將進行編碼轉換
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
}
// ...
}