Redis学习笔记&源码阅读--字典-概念

申明

  • 本文基于Redis源码5.0.8
  • 本文内容大量借鉴《Redis设计和实现》和《Redis5设计与源码分析》

概念

字典主要是用来存储健值对的一种数据结构,详细的概念不赘述了,我相信你不是为了释义来看我的博客。在Redis中字典有如下特征:

  1. 可以存储海量数据,键值对是映射关系,可以根据键以O(1)的时间复杂度取出或插入关联值。
  2. 键值对中键的类型可以是字符串、整型、浮点型等,且键是唯一的。
  3. 键值对中值的类型可为String、Hash、List、Set、SortedSet。

《Redis5设计与源码分析》一书中对字典的核心组成进行了比较详细的讲解,大家有条件的建议去看看,我这里只是尝试简化梳理下。

能够实现O(1)时间复杂度去取值的,首先想到的是数组,即使是存储海量数据也可以按照下标以O(1)时间复杂度取值,满足特征1,此时一个字典的结构如下图所示:

但是数组的访问是以下标,而特征2中健的类型可以是字符串等其他类型,这个时候就需要对健做一些特殊操作,处理的过程我们称之为Hash。

Hash函数

Hash的作用是把任意长度的输入通过算法转换成固定类型、固定长度的值,相同的输入经过Hash计算后得到相同的输出,不同的输入经过Hash后一般情况下得出的都是不一样的输出,小概率情况下会得到相同的输出(发生碰撞)。
这个时候我们可以将健通过Hash函数转换成整数类型的值了,但是Hash函数计算后的整数非常大,我们不能直接将其作为下标使用,那怎么办呢?最简单的方法就是对数组的容量求余,使用余作为下标值访问数组,此时我们就需要引入一个数组容量的字段到字典中,既然有容量,那么每次插入前是不是要判断当前容量是否足够?还需要引入一个当前使用量字段,此时字典的结构如下图所示:
在这里插入图片描述
这个方法不是完美的,因为可能出现不同的值求余结果一样,这种情况和刚才说的使用Hash计算出相同的结果统称为Hash冲突。

Hash冲突

为了处理Hash冲突,数组中的元素除了保存实际的值以外还要保存键的值和next指针用于指向冲突的下一个键值对,next指针可以把冲突的键值对串成单链表,“键”信息用于判断是否为当前要查找的键。此时字典的机构如下图所示:
在这里插入图片描述
为了保证键是唯一的,可以在代码中保证,每次插入时都先查询一次,这样特征2就实现了,特征3的实现比较简单,可以将字典中的具体值改为一个指针,指向任意内存。
经过上面的讲解,对字典的构成应该有了一个初步的认识,那么接下来我们看看在Redis中字典是怎么被实现的。

Redis中字典的实现

在讲解Redis中字典的构成前,我希望大家铭记字典的结构分层,这有助加速你了解字典,那就是
字典,Hash表,链表!!!
字典,Hash表,链表!!!
字典,Hash表,链表!!!

在我们介绍各个功能时,应该在脑海中自己思考这属于哪一层,我是这么做的,并且觉得很有帮助,希望对你也如此。
这里不按照由外向里的逻辑逐步深入Redis结构,而是直接先介绍字典的核心Hash表,再了解封装了Hash表的字典和Hash表维护的元素链表。

Hash表

Redis源码中Hash表的结构如下所示:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table; //指针数组,保存kv数据
    unsigned long size;//table的size,主要不是table中保存kv对的个数,table是一个数组,size是table数组的长度
    unsigned long sizemask;//掩码,其值永远等于size-1,用于在计算hash值后进行位运算计算新元素保存在table数组的哪些元素内(table保存的实际是一个链表)
    unsigned long used;//整个table保存了多少个kv对,主要用于扩缩容时使用
} dictht;

table是一个指针数组,数组中每一个元素保存的是一个指向entry的指针,entry中包括具体的key和value,也包括了一个指向了下一个元素的next指针,所以我们将table的元素在概念上理解为一个链表是比较合适的。一个entry具体的格式后面会介绍。
size指的是table数组的长度,sizemask是掩码,其值永远都是size-1,为什么要设计这个变量呢?我们知道计算一个新entry在table中保存的具体位置,是先使用Hash函数计算出一个值,然后对size求余,但是求余运算实际上是相对比较消耗资源的,Redis为了提高性能是做了特殊的优化。我们以实际例子来看,假设table的size是4,sizemask就是3,其二进制表示就是11,然后Hash值和11的与运算结果就是Hash值对size求余的结果。Redis就是利用了size为2^n时的这种特性来优化求余运算的,所以在Redis中table的size只会是4,8,16,32,64,128…这种,sizemask对应为3,7,15,63,127,对应的二进制是11,111,1111,11111…。其实这种操作是很常见的。
used表示的是table中所有链表保存元素的数量,其作用是在计算字典是否需要扩缩容时使用的。

字典

讲完了字典的核心数据结构,我们再来看看封装了Hash表的字典是什么结构的,先看它的源码:

typedef struct dict {
    dictType *type;//字典保存元素特定的操作函数集合
    void *privdata;//type中函数使用到的数据
    dictht ht[2];//Hash表数组
    long rehashidx;//默认值-1,表示字典当前不在rehash,否则表示字典正在进行rehash操作,值表示ht[0]中rehash执行到了该下标
    unsigned long iterators; //当前字典中执行的迭代器数量
} dict;

type字段是dictType类型的,先看下它的真身:

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);//字典对应的Hash函数
    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

将这些函数封装在dictType并提供给用户配置,就实现了字典保存不同类型数据的多态属性。字典中的privdata是配合这type一起使用的数据。
ht是包含了两个Hash表的数组,我们一般情况下只会使用到ht[0]的,只有当字典因为扩容或者缩容需要做rehash时才会短暂时间内使用,当扩容或者缩容结束后,新的Hash赋值给ht[0],ht[1]就不需要使用了。
rehashidx用来标记该字典是否在进行rehash,没进行rehash时,值为-1,否则,该值用来表示Hash表ht[0] 执行rehash到了哪个元素,并记录该元素的数组下标值。
iterators字段,用来记录当前运行的安全迭代器数,当有安全迭代器绑定到该字典时,会暂停rehash操作。Redis很多场景下都会用到迭代器,例如:执行keys命令会创建一个安全迭代器,此时iterators会加1,命令执行完毕则减1,而执行sort命令时会创建普通迭代器,该字段不会改变。

链表

其实讲到这里字典已经讲完了,已经没有新鲜的内容出炉了,提链表实际是为了提醒你Hash表中table这个指针的指针本质是一个一维数组,数组的每个元素是一个链表,牢牢的记住这一点有助于我们理解Redis对字典的各项操作。

最后我们看下一个完整字典的结构:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章