前言

关于 redis 的数据结构 dict[hashtable] 

相关介绍主要围绕着如下测试用例, 来看看 dict[hashtable] 的存储, 以及 相关的 api 

本文的 dict[hashtable] 相关代码 拷贝自 redis-6.2.0  

代码来自于 https://redis.io/ 

 

dict[hashtable] 这个数据结构映射到 jdk 这边 类似于 java.util.HashMap/HashTable 

很多操作 也是哈希表的基础操作, 呵呵 相当于又温习了一遍 

 

 

测试用例

//
// Created by Jerry.X.He on 2021/2/24.
//

#include <iostream>
#include "../libs/sds.h"
#include "../libs/dict.h"

using namespace std;

// dict funcs
uint64_t dictSdsHash(const void *key);

int dictSdsKeyCompare(void *privdata, const void *key1, const void *key2);

void dictSdsDestructor(void *privdata, void *val);

// clearCallback
void clearCallback(void *value);

int main(int argc, char **argv) {

    static dictType dt = {
            dictSdsHash,                /* hash function */
            NULL,               /* key dup */
            NULL,               /* val dup */
            dictSdsKeyCompare,          /* key compare */
            dictSdsDestructor,          /* key destructor */
            NULL,           /* val destructor */
    };

    sds name = sdsnew("name");

    // dictCreate
    dict *dict = dictCreate(&dt, NULL);
    dictAdd(dict, sdsfromlonglong(11), sdsfromlonglong(11));
    dictAdd(dict, sdsfromlonglong(12), sdsfromlonglong(12));
    dictAdd(dict, sdsfromlonglong(13), sdsfromlonglong(13));
    dictAdd(dict, sdsnew("key"), sdsnew("value"));

    // dictAdd
    int addResult = dictAdd(dict, name, sdsnew("jerry"));
    int duplicatedAddResult = dictAdd(dict, name, sdsnew("jerryUpdated"));

    // dictReplace
    int replaceResult = dictReplace(dict, name, sdsnew("jerryReplace"));

    // dictFind
    dictEntry *nameEntry = dictFind(dict, name);
    sds nameValue = (sds) (nameEntry->v.val);

    // dictFetchValue
    sds nameFetchValue = (sds) dictFetchValue(dict, name);

    // dictResize
    int resizedResult = dictResize(dict);

    // dictGetIterator & dictNext
    dictIterator *ite = dictGetIterator(dict);
    dictEntry *iteEntry = NULL;
    while ((iteEntry = dictNext(ite)) != NULL) {
        cout << (sds) iteEntry->key << " -> " << (sds) iteEntry->v.val << endl;
    }
    dictReleaseIterator(ite);

    // dictGetRandomKey
    dictEntry *randomEntry = dictGetRandomKey(dict);

    // dictGenHashFunction
    int dicGenHash = dictGenHashFunction("jerry", 5);
    int dicGenCaseHash = dictGenCaseHashFunction((const unsigned char *) "jerry", 5);

    // dictEmpty
    dictEmpty(dict, clearCallback);

    int x = 0;

}

// dictSdsHash
uint64_t dictSdsHash(const void *key) {
    return sdslen((sds) key);
}

// dictSdsKeyCompare
int dictSdsKeyCompare(void *privdata, const void *key1,
                      const void *key2) {
    int l1, l2;
    DICT_NOTUSED(privdata);

    l1 = sdslen((sds) key1);
    l2 = sdslen((sds) key2);
    if (l1 != l2) return 0;
    return memcmp(key1, key2, l1) == 0;
}

// dictSdsDestructor
void dictSdsDestructor(void *privdata, void *val) {
    DICT_NOTUSED(privdata);

    sdsfree((sds) val);
}

void clearCallback(void *value) {
    cout << "clearCallback" << endl;
//    cout << value << endl;
}

 

 

数据结构

06 关于 dict_map

 

 

dictCreate 

创建一个 dict 并初始化, 传入的 type 为 dict 所需要的一系列的业务函数 

比如 如何计算给定的元素的 hash, key 怎么比较 等等 

06 关于 dict_map_02

 

 

dictAdd 

可以看出的是 在断点的位置, dict 里面又四个元素, 都存储在 ht[0] 里面 

三个元素存储在 ht[0].table[2] 里面, 一个元素存储在 ht[0].table[3] 里面 

然后 我们现在再往 dict 里面添加一个元素, 会触发 dict 的扩容, 所以我们这里就 dictAdd 和 扩容的方式一起来看 

06 关于 dict_数据_03

 

向 dict 中添加一个 entry(key, value)

如果 key 对应的 entry 已经存在了[dictAddRaw返回NULL], 则返回 DICT_ERR 

可以看出 value 的值是后面才设置到 entry 里面去的 

06 关于 dict_hash_04

 

我们先概览一下 dictAdd 的逻辑, 之后 再来稍微看一下 扩容的部分 

如果正在 rehash, 单步 rehash 

根据 key 计算 hash, 然后计算 key 应该存放的 bucket 的索引, key 已经存在直接返回  

如果是在 rehash, 关联 ht[1], 然后创建 entry, 并初始化, 头插法插入到 table[index], 更新 th->table[index] 

设置 entry 的 key 

06 关于 dict_dict_05

 

 

关于扩容 

扩容之前的情况

可以看到上面添加了 "name" -> "jerry" 整个 dict 处于 rehash 的阶段 

所以将新加入的 "name" -> "jerry" 放到了 ht[1] 里面 

那么我们看一下 此时的 dict 里面的 ht[0], ht[1] 的情况  

可以看出的是 ht[0] 的里面的数据 还是和之前是一样的, ht[0].table[2] 里面有三个元素, ht[0].table[3] 里面有一个元素, ht[0] 里面合计四个元素 

ht[1] 里面 ht[1].table[4] 里面有一个元素, ht[1] 里面合计有一个元素 

06 关于 dict_map_06

 

扩容的判断是在哪里呢, 以及扩容的具体细节怎么的呢? 

可以看到的是 扩容的条件是 已经有的元素数量 大于等于 bucket 的数量 并且 dict_can_resize[dict扩容标记, 默认为 true], 或者 (已经有的元素数量/bucket 的数量) 比率超过了 dict_force_resize_ratio(默认为5)

    下面的 dictTypeExpandAllowed 是预留的一个限制规则, 通过 type 传入进来, 如果 expandAllowed 为 NULL, 默认返回 true 

06 关于 dict_dict_07

 

扩容之后的结构呢? 

具体扩容之后的 ht 的 bucket 数量是通过 size 来计算的, 相当于是 翻倍 

分配了新的 hashtable 之后, 初始化 ht[1] 为新的 hashtable 

06 关于 dict_dict_08

 

关于单步 rehash 

调用的是 _dictRehashStep(d), 每一次处理一个 bucket 的数据 

这个和 java.util.HashMap/HashTable 相比的差异就是, 前者是 在一定的操作下面来一部分一部分的来触发 rehash, 后者是在 expand 的过程中, 会创建新的 hashtable, rehash, 更新 hashtable, 整套流程做完 

随着数据量越来越大, 显然是 dict 这里的处理会有更快的响应处理, 一次处理一部分, 直到所有的数据处理完成, hash 函数足够分散的场景下面, 一个 bucket 上面最好的期望应该是只有一个元素, 单步 rehash 会非常快 

 

在那些情况下会推动 单步 rehash 呢?

1. dictAddRaw 添加元素的时候, dictAdd, dictReplace 都会调用 

2. dictDelete, dictUnlink 删除元素的时候 

3. dictFind 查询 entry 的时候, dictFetchValue 会调用 dictFind 

4. dictGetRandomKey/dictGetSomeKeys 获取元素的时候  

5. dictRehashMilliseconds 获取元素的时候[给定的时间限制内, 多次调用, 100步作为单位] 

 

 

单步 rehash 的逻辑

可以看到是 传入一个 n, 表示 n step, _dictRehashStep 传入的 n 为 1 

一个 step 就是迁移 ht[0] 里面的 rehashidx 或者下一个 有元素的 bucket 的数据迁移到 ht[1] 里面去 

如果 ht[0] 里面的元素迁移完毕了, 更新 rehash 状态, 切换 ht[0] 的数据为最新的 ht[1], 重置临时表 ht[1] 

06 关于 dict_dict_09

 

调试真实的单步 rehash

回到我们的测试用例, 第一个 dictAdd 会触发 rehash 

可以看出的是 ht[0] 的里面的数据 还是和之前是一样的, ht[0].table[2] 里面有三个元素, ht[0].table[3] 里面有一个元素, ht[0] 里面合计四个元素 

ht[1] 里面 ht[1].table[4] 里面有一个元素, ht[1] 里面合计有一个元素 

ht[0] 里面是有 table[2], table[3] 有数据, 因此 我们期望应该是第二个 dictAdd 会单步 rehash ht[0] 里面的 table[2] 的数据

接着的这一个 dictReplace 会单步 rehash ht[0] 里面的 table[3] 的数据 

接下来我们调试一下 这个流程  

06 关于 dict_初始化_10

 

执行了第二个 dictAdd 之后 

可以看到的是 ht[0].table[2] 的数据已经被清空了, ht[1].table[2] 里面装了数据了, 包含了三个元素[为什么我知道他会迁移到 ht[1].table[2], 呵呵 因为 这里的 hash 是我自定义的, 取得是字符串的长度]

ht[0] 里面只有 一个元素了, 在 table[3] 上面

ht[1] 里面有四个元素, 三个是在 table[2]上面, 刚才 单步 rehash 过去的, 一个是在 table[4]上面, 第一个 dictAdd 新增上去的 

06 关于 dict_数据_11

 

执行了第一个 dictReplace 之后 

ht[0] 上面还有 table[3] 需要 rehash, 将 ht[0].table[3] rehash 了之后, 其数据会移动到 ht[1].table[3] 或者 ht[1].table[7] 上面 

table[3] 处理完成之后, ht[0] 上面的所有的数据 就已经 rehash 完毕了, 可以执行上面的 切换 ht[0] 为 ht[1], 重置 ht[1] 的相关处理了 

可以看到的是 ht[1] 上面解析各个 table 的数据已经报错了, 因为这里执行了 单步 rehash 之后 rehash 已经完成了, 重置了 ht[1], 导致这里的 watch 报错 

可以看到的是 ht[0].table[2] 上面三个元素, ht[0].table[3] 上面一个元素, ht[0].table[4] 上面一个元素, 合计五个元素  

至此 一个简单的 单步 rehash 的调试, 就调试完了 

06 关于 dict_hash_12

ht[0] 的完整数据如下 

06 关于 dict_hash_13

 

 

dictReplace 

dictAdd 一个 entry(key, value), 如果新增成功 设置 entry.value 

否则 更新已有的 entry 的 value 

06 关于 dict_map_14

 

 

dictFind

如果是正在 rehash, 单步 rehash
计算 key 对应的 hash, 取模计算哪一个 bucket[两个 hash 表中查找]
遍历 bucket 根据 key 获取 entry 返回

06 关于 dict_hash_15

 

 

dictFetchValue

dictFind 找到 entry, 获取 value 返回

06 关于 dict_map_16

 

 

dictResize

根据 dict->used 来进行 resize
如果 dict->ht[0] 为空, 更新 dict->ht[0] 为 resize 之后的 hashtable
如果 dict->ht[0] 不为空, 更新 dict->ht[1] 为 resize 之后的 hashtable, 并设置为需要进行 rehash[然后后面的数据会在之后的业务处理过程中进行 单步 rehash]

06 关于 dict_hash_17

 

 

dictGetIterator/dictGetSafeIterator 

初始化一个 dictIterator, 并初始化 

06 关于 dict_数据_18

 

初始化一个 dictIterator, 并初始化, 更新 safe 标记为 true 

06 关于 dict_dict_19

 

dictNext

如果 ite->entry[某一个 bucket 上面的节点] 已经初始化过了, 不断的向后迭代
否则 初始化 ite->entry, ite->fingerprint 记录的是 ht[0], ht[1] 的状态, 如果是 安全迭代器, 暂停单步 rehash 
如果迭代完当前 ht 的最后一个 bucket, 判断是否是在 rehash, 切换 ht[1]
如果 不在 rehash, 或者 遍历完 ht[1], 返回 NULL
某一个 bucket 迭代完成, 更新 ite->entry 为 NULL, 向下一个 bucket 迭代

06 关于 dict_初始化_20

 

dictReleaseIterator

如果是安全迭代器, 恢复rehash的处理 

如果是不安全的迭代器, 断言[确保程序对于不安全的迭代器使用正常]确保 迭代期间 dict 的状态没有发生变化 

清理掉 iterator 

06 关于 dict_dict_21

 

 

dictGetRandomKey

先选择 bucket, 随机从 ht[0], ht[1] 选择一个 bucket

获取这个 bucket 的长度, 然后 随机选择一个元素  

06 关于 dict_初始化_22

 

 

dictEmpty

清理掉 ht[0], ht[1], 并重置 dict 的状态 

06 关于 dict_初始化_23

 

遍历整个 hashtable 的所有的 bucket 

清理掉所有的节点 

清理的处理过程中定期调用 callback, 传入 dict.privdata 作为参数 

清理掉 ht.table, 重置 ht   

06 关于 dict_数据_24