1. 程式人生 > >Redis 設計與實現 8:五大資料型別之雜湊

Redis 設計與實現 8:五大資料型別之雜湊

雜湊物件的編碼有兩種:`ziplist`、`hashtable`。 ## 編碼一:ziplist `ziplist` 已經是我們的老朋友了,它一出現,那肯定就是為了節省記憶體啦。那麼雜湊物件是怎麼用 `ziplist` 儲存的呢? 每次插入鍵值對的時候,在 `ziplist` 列表末尾,挨著插入 `field` 和 `value` 。如下圖: ![hash-ziplist 編碼結構](https://img-blog.csdnimg.cn/20210101130405685.png) ### 常見操作 增刪改查都涉及到一塊很類似的程式碼,那就是查詢。 redis 這幾個函式的查詢部分,幾乎都是直接複製貼上。。。可能有改動就有點難維護了。 #### 獲取 先從 ziplist 中拿到 field 的指標,然後向後一個節點就是 value > 找 `field` 的時候,`ziplistFind` 最後一個引數傳入的是 `1`,表示查一個節點後,跳過一個節點不查。 因為 `hash` 在 `ziplist` 中的存就是 `field` `value` **挨著存**的,我們查的是 `field`,所以要跳過 `value`。 ```c 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; } ``` #### 刪除 刪除其實就是先查詢,後刪除 ```c 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; } ``` #### 插入 / 更新 一切盡在註釋中 ```c 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](https://www.cnblogs.com/chenchuxin/p/14191156.html) 已經寫了,包括了 dict 基本操作的原始碼解讀。 其結構就相當複雜啦,再來複習一下,如下圖: ![hash-hashtable 編碼](https://img-blog.csdnimg.cn/20210103215242140.png) ### 常見操作 #### 獲取 `hashtable` 編碼本身的思路跟 `dict` 的基本 api 很契合,所以程式碼比較整潔。獲取值就是直接呼叫 ` dict.c/dictFind` 而已。 前文 [Redis 設計與實現 4:字典 dict](https://www.cnblogs.com/chenchuxin/p/14191156.html) 已經對 `dict` 的查詢原始碼分析過,感興趣的讀者可以看看。 ```c 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](https://www.cnblogs.com/chenchuxin/p/14191156.html) 已經對 `dict` 的刪除原始碼分析過,感興趣的讀者可以看看。 ```c 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`)。 ```c // 所有權定義 #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` 函式中,都會呼叫到編碼的轉換。程式碼如下 ```c 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); // ... } ``` ```c // 檢查長度超過 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; } } } ``` ```c 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); } // .