Redis 設計與實現 8:五大數據類型之哈希

哈希對象的編碼有兩種:ziplisthashtablehtml

編碼一:ziplist

ziplist 已是咱們的老朋友了,它一出現,那確定就是爲了節省內存啦。那麼哈希對象是怎麼用 ziplist 存儲的呢?
每次插入鍵值對的時候,在 ziplist 列表末尾,挨着插入 fieldvalue 。以下圖:redis

hash-ziplist 編碼結構

常見操做

增刪改查都涉及到一塊很相似的代碼,那就是查找。
redis 這幾個函數的查找部分,幾乎都是直接複製粘貼。。。可能有改動就有點難維護了。api

獲取

先從 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 基本操做的源碼解讀。指針

其結構就至關複雜啦,再來複習一下,以下圖:code

hash-hashtable 編碼

常見操做

獲取

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 相似。也是先查看是否存在,若是已存在,則刪除原來的值,再從新設置新值; 若是不存在,則添加一整個鍵值對。

這裏比較有趣的是,對 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);
    }
    // ...
}
相關文章
相關標籤/搜索