如何实现一个性能优异的Hash表

69 阅读2分钟

在实际应用 Hash 表时,当数据量不断增加,它的性能就经常会受到哈希冲突和 rehash 开销的影响。

针对哈希冲突,Redis 采用了链式哈希,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到;对于 rehash 开销,Redis 实现了渐进式 rehash 设计,进而缓解了 rehash 操作带来的额外开销对系统的性能影响。

Redis 如何实现链式哈希?

Redis 在每个 dictEntry 的结构设计中,除了包含指向键和值的指针,还包含了指向下一个哈希项的指针。这就是用来实现链式哈希的.

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

Redis 如何实现 rehash?

首先,Redis 准备了两个哈希表,用于 rehash 时交替保存数据。

其次,在正常服务请求阶段,所有的键值对写入哈希表 ht[0]。

接着,当进行 rehash 时,键值对被迁移到哈希表 ht[1]中。

最后,当迁移完成后,ht[0]的空间会被释放,并把 ht[1]的地址赋值给 ht[0],ht[1]的表大小设置为 0。这样一来,又回到了正常服务请求的阶段,ht[0]接收和服务请求,ht[1]作为下一次 rehash 时的迁移表。

image.png

Redis 用来判断是否触发 rehash 的函数是 _dictExpandIfNeeded。

实际上,_dictExpandIfNeeded 函数中定义了三个扩容条件。

  • 条件一:ht[0]的大小为 0。
  • 条件二:ht[0]承载的元素个数已经超过了 ht[0]的大小,同时 Hash 表可以进行扩容。
  • 条件三:ht[0]承载的元素个数,是 ht[0]的大小的 dict_force_resize_ratio 倍,其中,dict_force_resize_ratio 的默认值是 5。

当我们往 Redis 中写入新的键值对或是修改键值对时,Redis 都会判断下是否需要进行 rehash。

Redis 在执行 rehash 操作时,对 Hash 表扩容的思路也很简单,就是如果当前表的已用空间大小为 size,那么就将表扩容到 size*2 的大小。函数:dictExpand

渐进式 rehash 的意思就是 Redis 并不会一次性把当前 Hash 表中的所有键,都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝 Hash 表中一个 bucket 中的哈希项。这样一来,每次键拷贝的时长有限,对主线程的影响也就有限了。

关键函数:dictRehash 和 _dictRehashStep。

image.png


此文章为10月Day3学习笔记,内容来源于极客时间《Redis 源码剖析与实战》