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

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

概述:

在redis中有不少地方都用到了字典结构,好比咱们redis中的哈希结构就是用的字典实现的,而字典结构的底层事使用哈希表实现的(😄有点绕~),XDM能够经过这篇文章了解到如下知识点。算法

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

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

  3. redis的rehash原理和实现markdown

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

哈希表

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

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

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

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

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

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

哈希表节点

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和他的源码实现。

敬请期待~

相关文章
相关标签/搜索