阅读 491

redis字典结构终结篇你确定不看吗?

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

概述:

在redis中有很多地方都用到了字典结构,比如我们redis中的哈希结构就是用的字典实现的,而字典结构的底层事使用哈希表实现的(😄有点绕~),XDM可以通过这篇文章了解到以下知识点。

  1. 哈希表、哈希表节点、以及字典的结构和实现。

  2. 哈希算法redis是如何解决哈希冲突的。

  3. redis的rehash原理和实现

哈希表、哈希表节点、以及字典的结构和实现

哈希表

哈希表的结构是这个样子的:

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;(这个是指向指针的指针)
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;
复制代码

table是一个指针数组,他里面的每个元素都指向一个哈希表节点dictEntry。

size 是当前哈希表的大小,也就是我们table数组中元素数

used 记录了哈希表目前已有节点(键值对)的数量

sizemask 属性的值总是等于size - 1 , 这个属性和哈希值一起决定一个键应该被放到数组的哪个索引上面。这个如果没有记错的话应该是用来rehash使用的。

哈希表节点

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;
复制代码

key就是我们哈希的key,指向了一个SDS对象或者是一个整数。

next是一个指针指向一个dictEntry对象,这个是为了解决哈希冲突的,当哈希冲突产生的时候,所有冲突的key的就会形成一条链表。

字典

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;
复制代码

type 是一个指向dictType结构的指针,每个dictType中都包含了操作不同类型字符串的方法,privdata则保存了这些方法需要的参数

dictType的结构如下:

typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;
复制代码

我们可以看到还有两个字段 ht[2]和rehashidx,正常情况下我们的数据都是在ht[0]中的。那ht[1]只有在rehash的时候才会使用,而rehashidx就是我们的rehash进度,当rehashidx为-1的时候就说明当前哈希表没有进行rehash操作。在redis源码中我们可以看到这种判断。

这是一个没有进行rehash的普通字典结构:

哈希算法redis是如何解决哈希冲突的

当两个或者多个的key通过哈希算法算出来的值是一样的时候我们就称这种是出现了哈希冲突,在上边介绍基本结构的时候就提到过这个next是一个指针指向一个dictEntry对象,当哈希冲突产生的时候,所有冲突的key的就会形成一条链表。当查询的时候就会按照这个链表一个一个往后查找到和需要获取的key相等的值,然后返回。

redis的rehash原理和实现

负载因子

首先我们要先介绍一下负载因子的概念,在我们对哈希表的增加和删除操作的过程中会导致我们的哈希表不断增大。从而导致ht[0].used / ht[0].size 比例失衡 这两个的概念需要看下上边的定义。

我们可以想一下这比例在什么时候这个哈希表是最优状态?

应该是1,因为如果是1空间没有浪费,也说明没有那么多的哈希冲突。

redis会根据这个比例判断我们哈希表是要扩展还是收缩。接下来我们大概看一下这个rehash的过程。

  1. 这第一步就要使用到我们的ht[1],先为它分配空间。具体分配多大的空间要取决于我们是要执行扩展还是收缩操作,还有就是当前ht[0]中的元素数量。(扩展操作ht[1]的大小是第一个大于等于ht[0].used * 2^n 如果是收缩操作大小为第一个大于等于ht[0].used的2^n。 这个我们解释一下,比如当前我们的used是6,我们需要申请的空间就是(6*2 = 12)<2^4是16。应该都可以理解哈~)。
  2. 然后就是将我们ht[0]上的key使用新的重新计算哈希值和索引值,并按照新的索引值放到我们的ht[1]上。
  3. 当我们的ht[0]上的所有键值对都迁移到ht[1]之后ht[0]是空表了,此时将ht[0]指向ht[1] ht[1]指向null 至此我们的rehash就完成了。

什么时候会进行rehash呢?

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于1进行扩展操作。

  2. 服务器正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于5进行扩展操作。

  3. 当哈希表的负载因子小于0.1时, 程序自动开始对哈希表执行收缩操作

具体为什么 BGSAVE 命令或者 BGREWRITEAOF 命令执行期间负载因子的判断值不一样,我猜测是因为性能的考虑,xdm可以查一下相关资料评论下

我们上边说的rehash并不是一下就全部完成,如果我们有个很大的哈希表那rehash过程是要消耗很长时间的我们的redis是单线程,这种消耗主线程时间的逻辑很致命的。redis在这个地方的设计真的是很优雅,xdm肯定发现我们还有一个rehashidx字段没用到,字段就是我们后边要写的渐进式rehash中要使用到的重要内容。

总结

xdm这篇先到这,主要整理了下redis字典底层哈希表的实现,以及rehash的过程。接下来会整理一下redis的优雅设计渐进式rehash和他的源码实现。

敬请期待~

文章分类
后端
文章标签