[ Redis14篇]字典之渐进式Hash结构

2021/12/6 2:17:23

本文主要是介绍[ Redis14篇]字典之渐进式Hash结构,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1.Redis字典Hash底层数据结构?

Redis 字典 hash 的数据结构底层是一个 dict 对象。

  • 用key值计算hashkey
  • 元素插入到某个hash链上(拉链法解冲突)

dict的结构:

  • dictht table 哈希表内部的table结构
    • 使用了拉链法来解决哈希冲突
    • 其实是一个指向数组的指针,数组中的每一项都是entry链表的头结点。
  • dictht ht[2]:在dict的内部,维护了两张哈希表(可以理解为2个数组)。
    • 作用等同于是一对滚动数组,ht[0]表是旧表,ht[1]表是新表扩容时候使用,是ht[0]的2倍。
    • 当hash 表的大小需要动态改变的时候,旧表中的元素就往新开辟的新表中迁移,
    • 当下一次变动大小,当前的新表又变成了旧表,以此达到资源的复用和效率的提升。
  • rehashidx(扩容标记字段)
    • 因为是渐进式的哈希,数据的迁移并不是一步完成的,所以需标记来表示当前的扩容进度。
    • 当rehashidx为-1时,代表没有操作。
      在这里插入图片描述

2.如何计算的Hash 值

Redis 使用** MurmurHash2 算法**来计算键的哈希值。
MurmurHash 算法最初由 Austin Appleby 于 2008 年发明, 这种算法的优点在于, 即使输入的键是有规律的, 算法仍能给出一个很好的随机分布性, 并且算法的计算速度也非常快。

3.如何解决哈希冲突

答:数组+链表
插入一条新的数据时,会进行哈希值的计算,如果出现了hash值相同的情况,
Redis 中采用了连地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个next 指针,多个哈希表节点可以使用next 构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来解决hash值冲突的问题。

这里使用的是头插法,因为哈希表节点中没有记录链表尾节点位置(复杂度为 O(1))

4.什么是渐进式扩容(Rehash)

渐进式rehash指的是,redis hash 结构维护了2个表,一个是旧表,一个是新表大小是旧表的2倍。
当需要扩容的时候,并不是一次性操作全量数据,而是分散在了 读、写、删 的操作中,逐步迁移,直到迁移完成。
ps: 读/删:先去ht[0]找,找不到去ht[1]。 2)写:直接写在ht[1]

Redis既然采用的是 渐进式rehash为了提高性能,若每次rehash都操作全量数据,是非常耗性能的

总结:分而治之,避免了集中式rehash 带来的庞大计算量。

5.什么时候进行扩容(Rehash)?

在redis中,字典里的哈希会根据以下两种情况进行扩容:

  • 没有在 执行持久化( bgsave 命令或者 bgrewriteaof 命令) 并且哈希表的负载因子>= 1 ;
  • 正在 执行持久化 (bgsave 命令或者bgrewriteaof命令) 并且哈希表的负载因子>= 5 ;

其中哈希表的负载因子可以通过公式:

  • 负载因子 = 哈希表已保存节点数量 / 哈希表大小
  • load_factor = ht[0].used / ht[0].size

6.为什么扩容因为是否在持久化的负载因子不同

因为在执行 bgsave 命令或者bgrewriteaofF 命令的过程中
Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制 (copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作。这可以避免不必要的内存写入操作,最大限度节约内存。
另一方面,当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

7.Redis Rehash机制优化

在Redis满容驱逐状态下,如何避免因Rehash而导致Redis抖动的这种问题呢?

  • 在Redis Rehash实现的逻辑上,加上了一个判断条件,若现有的剩余内存不够触发Rehash操作所需申请的内存大小,则不进行Resize操作;
  • 通过提前运营进行规避,比如容量预估时将Rehash占用的内存考虑在内,或者通过监控定时扩容。

8.Rehash的过程中有数据变化怎么办

  • 关于字典的操作无非就是四个,增删改查。
  • 读则先在h[0]中查找,查询不到再到h[1]
  • 新增-则直接新增到h[1]
  • 删除先删除h[0],再删除h[1]
  • 修改直接修改h[1]

保证redis在h[0]上是只少不多,所有的记录逐步被迁移到h[1]

9.Redis单线程渐进式如何Rehash(扩容)

Rehash操作分为两种
扩展:当负载因子较大时,应该扩大 dictht::size 以降低平均长度,加快查询速度 。
收缩:当负载因子较小<0.1时,应该减小 dictht::size 以减少对内存的浪费 。

采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量
渐进式rehash 的详细步骤:

  • 为ht[1] 分配空间是ht[0]的两倍,让字典同时持有ht[0]和ht[1]两个哈希表
  • 索引计数器变量 rehashidx,原先是-1 ,现将它的值设置为0,表示rehash 开始
  • 在rehash 进行期间,每次对字典执行CRUD操作时,除了执行指定的操作外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一。
    • 读/删:**先去ht[0]找,找不到去ht[1] ; **写:直接写在ht[1]中
    • rehash是以bucket(桶)为基本单位进行渐进式的数据迁移的,每步完成一个bucket的迁移,直至所有数据迁移完毕。
    • 一个bucket对应哈希表数组中的一条entry链表。
    • 新版本的dictRehash()还加入了一个最大访问空桶数(empty_visits)的限制来进一步减小可能引起阻塞的时间。
  • 最后判断一下旧表是否全部迁移完毕,若是,则回收空间,重置旧表,重置渐进式哈希的索引 rehashidx=-1,
  • 否则用返回值告诉调用方,dict内仍然有数据未迁移。


这篇关于[ Redis14篇]字典之渐进式Hash结构的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程