哈希對象的編碼有兩種:ziplist
、hashtable
。html
ziplist
已是咱們的老朋友了,它一出現,那確定就是爲了節省內存啦。那麼哈希對象是怎麼用 ziplist
存儲的呢?
每次插入鍵值對的時候,在 ziplist
列表末尾,挨着插入 field
和 value
。以下圖:redis
增刪改查都涉及到一塊很相似的代碼,那就是查找。
redis 這幾個函數的查找部分,幾乎都是直接複製粘貼。。。可能有改動就有點難維護了。api
先從 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
編碼用的是字典 dict
做爲底層實現,關於 dict
,具體的前文 Redis 設計與實現 4:字典 dict 已經寫了,包括了 dict 基本操做的源碼解讀。指針
其結構就至關複雜啦,再來複習一下,以下圖:code
hashtable
編碼自己的思路跟 dict
的基本 api 很契合,因此代碼比較整潔。獲取值就是直接調用 dict.c/dictFind
而已。server
前文 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); } // ... }