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

小新是也發表於2021-01-03

雜湊物件的編碼有兩種:ziplisthashtable

編碼一:ziplist

ziplist 已經是我們的老朋友了,它一出現,那肯定就是為了節省記憶體啦。那麼雜湊物件是怎麼用 ziplist 儲存的呢?
每次插入鍵值對的時候,在 ziplist 列表末尾,挨著插入 fieldvalue 。如下圖:

hash-ziplist 編碼結構

常見操作

增刪改查都涉及到一塊很類似的程式碼,那就是查詢。
redis 這幾個函式的查詢部分,幾乎都是直接複製貼上。。。可能有改動就有點難維護了。

獲取

先從 ziplist 中拿到 field 的指標,然後向後一個節點就是 value

field 的時候,ziplistFind 最後一個引數傳入的是 1,表示查一個節點後,跳過一個節點不查。
因為 hashziplist 中的存就是 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 基本操作的原始碼解讀。

其結構就相當複雜啦,再來複習一下,如下圖:

hash-hashtable 編碼

常見操作

獲取

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 類似。也是先檢視是否存在,如果已存在,則刪除原來的值,再重新設定新值; 如果不存在,則新增一整個鍵值對。

這裡比較有趣的是,對 fieldvalue 定義了所有權 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 編碼。


hsetnxCommandhsetCommand 函式中,都會呼叫到編碼的轉換。程式碼如下

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);
    }
    // ...
}

相關文章