Redis数据结构 - 字典(三)

概述

字典,实质是一个K-V数据库,经常使用Java语言的开发者可以直接理解为集合MAP。字典中每一个键都是独一无二的,可以在程序中根据键来进行查找、更新、插入操作。同样因为C中并没有这种数据结构,Redis构建了自己对字典实现。字典在Redis中的应用非常广泛,Redis数据库就是使用字典来实现的,对数据进行CURD也是构建在对字典的操作之上。

定义

字典底层使用的是哈希表,一个哈希表中可以有多个哈希表节点,每个节点保存K-V键值对;

1. 哈希表

下面一个是哈希表的结构体,内部包含了四个属性值,分别是哈希表数组、哈希表大小、哈希表大小掩码、已有节点数量值;
在这里插入图片描述

2. 哈希表节点

哈希表节点使用dictEntry结构表示,每一个结构保存着一个键值对;k是节点的键,v是节点的值;其中值v可以是一个指针,也可以是一个uint64_t整数或int64_t整数。next属性是指向另外一个节点的指针。
在这里插入图片描述

3. 字典

Redis中字典结构如图所示,包含四个属性;
在这里插入图片描述
下图展示的是一个完整的字典结构:
在这里插入图片描述

4. 哈希算法

把一个键值对放入到字典之前,先要根据键计算出哈希值以及索引值,然后将键值对包装成哈希节点放入哈希表数组指定索引上面;
Redis计算哈希值和索引值的方法如下:

  • 将一个键值对K0、V0添加到字典,首先计算K0的哈希值(hash = dict -> type ->hashFunction(K0))
  • 假设计算出哈希值为8,继续计算K0的在数组中的索引值 (index = hash&dict->ht[0].sizemask = 0)
  • 计算出K0索引值0,这表示包含键值对K0,V0的节点应该放在索引0位置上;
    在这里插入图片描述

5. 哈希冲突

当有两个或者两个以上的键被分配到哈希表数组的同一个索引上,我们称这些键发生了冲突。
Redis使用“链地址”思想来解决冲突,每个节点都有一个next指针,多个节点构成一个单向链表;针对键值相同的节点使用单项链表连接起来,解决冲突问题;熟悉Java的开发者会联想到HashMap底层实现,针对哈希冲突,两者是采用同样的解决方案。
下图中K1与K2键计算得索引值都是2,因此使用next指针进行串联,得到链表结构;
在这里插入图片描述

6. Rehash与渐进式Rehash

Rehash,即重新散列。随着对哈希表频繁操作,键值对的数量会逐渐增多或者减少,为了让哈希表负载因子维持在一个合理范围内,需要对表进行扩展或收缩。

  • 上面提到,每个字典中哈希表ht[2]都有两个元素, ht[0]为默认哈希表, ht[1]为辅助哈希表,常用于辅助默认哈希表完成rehash过程;
  • rehash开始,首先为ht[1] 分配空间,ht[1]的空间大小取决于ht[0] 表已使用元素的大小;无论是扩展还是收缩,哈希表的大小总是等于第一个大于等于ht[0].used * 2的次幂的值;例如:当前已经使用的节点大小为4,那么扩展后节点大小为 4 *2的1次方 = 8;

哈希表扩展或者收缩时机

当满足以下任一条件时,程序会自动进行Rehash操作:

  • 服务此时没有执行BGSAVE或者BGREWRITEAOF操作,并且哈希表的负载因子大于等于1
  • 服务器正在执行上述两个命令,并且负载因子大于等于5

注意:负载因子 = 已保存节点大小 / 哈希表大小,当哈希表的负载因子小于0.1时,程序开始对哈希表进行收缩操作。

渐进式ReHash

Redis将ht[0] 里面所有的数据rehash到ht[1], 整个动作不是一次性、集中性的完成的,而是采用分而治之的方式将表数据的迁移分配到添加、删除以及更新操作上,避免了rehash过程占用过大的系统资源;例如:在哈希表中查找一个键,程序会现在ht[0]上面查找,如果没有找到再去ht[1] 查找,数据的查找、删除、更新都会再两个表中进行操作,而插入只需要操作ht[1]一个表。

扫描二维码关注公众号,回复: 6225916 查看本文章

总结

  • 字典使用场景很丰富,例如:哈希键、数据库;
  • 字典用作数据库或者哈希键的实现时,Redis使用MurmmurmHash2算法来计算键的哈希值;
  • 哈希表中键的冲突使用“拉链法”思想来解决的;
  • 哈希表扩展和收缩rehash过程不是一次性完成的,而是有一个渐进的过程;

参考资料

《Redis设计与实现》第三章 字典

能力有限,如果文中存在不妥当的内容,恳请及时指正!

猜你喜欢

转载自blog.csdn.net/weixin_42677165/article/details/89963533