工作生活

「Redis源码解读」—数据结构(二)哈希表

2019-06-30  本文已影响0人  wh4763

Redis的字典使用哈希表作为底层实现

知识点

1.数据结构

2.哈希

3.rehash

4.渐进式rehash

数据结构定义

//哈希表
typedef struct dictht {
    dictEntry **table;             // 哈希表数组
    unsigned long size;            // 哈希表数组的大小
    unsigned long sizemask;        // 用于映射位置的掩码,值永远等于(size-1)
    unsigned long used;            // 哈希表已有节点的数量
} dictht;
//哈希节点
typedef struct dictEntry {
    void *key;                  // 键
    union {                     // 值
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     // 指向下一个哈希表节点,形成单向链表
} dictEntry;
//字典
typedef struct dict {
    dictType *type;                        // 和类型相关的处理函数
    void *privdata;                        // 上述类型函数对应的可选参数
    dictht ht[2];                          // 两张哈希表,ht[0]为原生哈希表,ht[1]为 rehash 哈希表
    long rehashidx;                        // 当等于-1时表示没有在 rehash,否则表示 rehash 的下标
    int iterators;                         // 迭代器数量(暂且不谈)
} dict;

概述

索引定位

当要将一个新的键值对添加到字典里面或者通过键查找值的时候都需要执行哈希算法,主要是获得一个需要插入或者查找的dictEntry 所在下标的索引,具体算法如下:
1.计算得到该键对应的哈希值
2.将哈希值和哈希表的 sizemask 属性做位与,得到索引值 index,其中 ht[x] 可以是 ht[0] 或者 ht[1]

冲突解决

哈希的冲突一定发生在键值对插入时:
判断当前的字典是否在进行 rehash,如果是,则执行一步 rehash,否则忽略。判断 rehash 的依据就是 rehashidx 是否为 -1;
通过 _dictKeyIndex 找到一个索引,如果返回-1表明字典中已经存在相同的 key;
根据是否在 rehash 选择对应的哈希表;
分配哈希表节点 dictEntry 的内存空间,执行插入,插入操作始终在链表头插入,这样可以保证每次的插入操作的时间复杂度一定是 O(1) 的,插入完毕,used属性自增;

rehash

随着字典操作的不断执行,哈希表保存的键值对会不断增多(或者减少),为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,需要对哈希表大小进行扩展或者收缩。

1.负载因子
当前已使用结点数量除上哈希表的大小,即:

load_factor = ht[0].used / ht[0].size

2.哈希表扩展

渐进式rehash

扩展或者收缩哈希表的时候,需要将 ht[0] 里面所有的键值对 rehash 到 ht[1] 里,当键值对数量非常多的时候,这个操作如果在一帧内完成,大量的计算很可能导致服务器宕机,所以不能一次性完成,需要渐进式的完成。

渐进式 rehash 的详细步骤如下:
1、为 ht[1] 分配指定空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表;
2、将 rehashidx 设置为0,表示正式开始 rehash,前两步是在 dict.c/dictExpand 中实现的
3、在进行 rehash 期间,每次对字典执行 增、删、改、查操作时,程序除了执行指定的操作外,还会将 哈希表 ht[0].table中下标为 rehashidx 位置上的所有的键值对 全部迁移到 ht[1].table 上,完成后 rehashidx 自增。这一步就是 rehash 的关键一步。为了防止 ht[0] 是个稀疏表 (遍历很久遇到的都是NULL),从而导致函数阻塞时间太长,这里引入了一个 “最大空格访问数”,也即代码中的 enmty_visits,初始值为 n*10。当遇到NULL的数量超过这个初始值直接返回。
4.最后,当 ht[0].used 变为0时,代表所有的键值对都已经从 ht[0] 迁移到 ht[1] 了,释放 ht[0].table, 并且将 ht[0] 设置为 ht[1],rehashidx 标记为 -1 代表 rehash 结束。

上一篇 下一篇

猜你喜欢

热点阅读