redis学习笔记-hash原理

基本概念

字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现。通常有 map 之类的。

在redis使用中的特点:

  • 可以存储海量数据,键值对是映射关系,可以根据键以O(1)的时间复杂度取出或插入关联值。
  • 键值对中键的类型可以是字符串、整型、浮点型等,且键是唯一的。例如:执行set test "hello world"命令,此时的键test类型为字符串,如test这个键存在数据库中,则为修改操作,否则为插入操作。
  • 键值对中值的类型可为String、Hash、List、Set、SortedSet。

我们可以根据这几个特点来自己想一下会怎么实现。

  1. 根据 海量数据 我们可以保存数据的指针;
  2. 根据 O(1) 取值,可以使用数组;

那么连接起来就是 数组指针 。哈哈,机制!

但数组是通过下标来访问,在 redis 中它的 key 值 可不一定是整形的。所以这就需要引入 hash 函数来处理 它的 key 都可以转为整形。

hash函数

Hash一般翻译为“散列”,也有直接音译为“哈希”,作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值,换句话说,Hash函数可以把不同键转换成唯一的整型数据。散列函数一般拥有如下特征:

  1. 相同的输入经Hash计算后得出相同输出;
  2. 不同的输入经Hash计算后一般得出不同输出值,但也可能会出现相同输出值。

那么现在应该想怎么去定位一个key 在数组中的位置?

最简单的办法是,用Hash值与数组容量取余,会得到一个永远小于数组容量大小的值,此时的值也就恰好可以当作数组下标来使用,我们把取余之后的值称为键在该字典中的索引值,即“索引值==数组下标值”,拿到“键”的索引值后,我们就知道数组中哪个元素是用来存储键值对中的“值”了。

但此方法并不是完美的,还会出现一个问题,Hash冲突。在无限的输入并在有限的输出中,肯定会发生 不同的key 定位在同一个 index 上。

为了解决Hash冲突,所以数组中的元素除了应把键值对中的“值”存储外,还应该存储“键”信息和一个next指针,next指针可以把冲突的键值对串成单链表,“键”信息用于判断是否为当前要查找的键。

redis学习笔记-hash原理_第1张图片

类似如图所示。

在很多的编程语言中,它内置的 map 结构都是这样来设计的。

redis 字典设计

Redis的字典也是通过Hash函数来实现的,因为Redis是基于工程应用,需要考虑的因素会更多。

Redis字典实现依赖的数据结构主要包含了三部分:字典、Hash表、Hash表节点。字典中嵌入了两个Hash表,Hash表中的table字段存放着Hash表节点,Hash表节点对应存储的是键值对。

Hash表

redis学习笔记-hash原理_第2张图片

Hash表的结构体整体占用32字节,其中table字段是数组,作用是存储键值对,该数组中的元素指向的是dictEntry的结构体,每个dictEntry里面存有键值对。size表示table数组的总大小。used字段记录着table数组已存键值对个数。

sizemask字段用来计算键的索引值,sizemask的值恒等于size-1。

Hash表节点

redis学习笔记-hash原理_第3张图片

Hash表中元素结构体和我们前面自定义的元素结构体类似,整体占用24字节,key字段存储的是键值对中的键。v字段是个联合体,存储的是键值对中的值,在不同场景下使用不同字段。例如,用字典存储整个Redis数据库所有的键值对时,用的是*val字段,可以指向不同类型的值;再比如,字典被用作记录键的过期时间时,用的是s64字段存储;当出现了Hash冲突时,next字段用来指向冲突的元素,通过头插法,形成单链表。

字典

Redis字典实现除了包含前面介绍的两个结构体Hash表及Hash表节点外,还在最外面层封装了一个叫字典的数据结构,其主要作用是对散列表再进行一层封装,当字典需要进行一些特殊操作时要用到里面的辅助字段。

redis学习笔记-hash原理_第4张图片

字典这个结构体整体占用96字节,其中type字段,指向dictType结构体,里面包含了对该字典操作的函数指针,具体如下:

redis学习笔记-hash原理_第5张图片

  • privdata字段,私有数据,配合type字段指向的函数一起使用。

  • ht字段,是个大小为2的数组,该数组存储的元素类型为dictht,虽然有两个元素,但一般情况下只会使用ht[0],只有当该字典扩容、缩容需要进行rehash时,才会用到ht[1]。

  • rehashidx字段,用来标记该字典是否在进行rehash,没进行rehash时,值为-1,否则,该值用来表示Hash表ht[0] 执行rehash到了哪个元素,并记录该元素的数组下标值。

  • iterators字段,用来记录当前运行的安全迭代器数,当有安全迭代器绑定到该字典时,会暂停rehash操作。

完整结构如图所示:
redis学习笔记-hash原理_第6张图片

Hash数据类型的对象编码有两种,分别是OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT,也就是说它的底层是由两种数据结构来支持的:一种是ZipList,另一种是哈希字典。

Redis的Hash数据类型之所以使用OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT两种编码格式,是因为当一个Hash对象的键值对数据量比较小时,使用紧凑的数组格式可以节省内存空间。

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

hash-max-ziplist-entries的默认值为512,表示当Hash对象的键值对数量大于该值时使用OBJ_ENCODING_HT编码,否则使用OBJ_ENCODING_ZIPLIST编码。

hash-max-ziplist-value的默认值为64,表示Hash对象中的键值对存在键的长度或值的长度大于该值时使用OBJ_ENCODING_HT编码,否则使用OBJ_ENCODING_ZIPLIST编码。

Redis的Hash数据类型之所以这样设计,是因为当ZipList变得很大时会有下面几个缺点:

  1. 每次插入或修改引发的realloc(内存再分配)操作都会有更大的概率造成内存复制,从而降低性能。
  2. 一旦发生内存复制,内存复制的成本也相应增加,因为要复制更大的一块数据。
  3. 当ZipList数据项过多时,在它里面查找指定的数据项就会使性能变得很低,因为在ZipList中查找数据项需要依次执行遍历操作。

到这儿我们已经看完了 redis 的 hash 设计了,整体来说和其他语言的map 实现原理差不多!但是有很多的细节才是它的精髓。


欢迎关注我的公众号,分享golang日常,和学习笔记。

Go菜鸟:GolangNewBie

你可能感兴趣的:(Redis,哈希算法,redis,学习)