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

Posted king哥Java架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis字典结构终结篇你确定不看吗?相关的知识,希望对你有一定的参考价值。

概述:

在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对象或者是一个整数。

如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
[Java架构群]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的JAVA交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

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

最后

给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。

文档地址:一篇神文就把java多线程,锁,JMM,JUC和高并发设计模式讲明白了

码字不易,如果觉得本篇文章对你有用的话,请给我一键三连!关注作者,后续会有更多的干货分享,请持续关注!

以上是关于redis字典结构终结篇你确定不看吗?的主要内容,如果未能解决你的问题,请参考以下文章

npm包的更新说明,你还敢不看吗

还在苦恼学python不知道方向,你还不看吗?未来python就业方向

小朋友看了都会的二叉树,你确定不来看看吗?

全网最经典26道Spring面试题总结,看完这一篇你就懂了

单片机外围电路设计攻略(终结版)! 不看哭一年!

单片机外围电路设计攻略(终结版)! 不看哭一年!