Redis系列--渐进式哈希

Redis支持的数据结构有很多,其中dict的使用非常频繁,其实Redis的每一个数据库结构都是一个dict。dict使用哈希表实现,这也是Redis性能十分强悍的原因之一,增删改查的时间复杂度为O(1).

上图是我根据Redis源码中定义的数据结构及网上资料参考画的参考图。

随着Redis的操作越来越多,dict中保存的数据量也会动态变化,当数据量增加或者减少到一定的程度,为了让负载因子维持在一个合理的范围内,Redis就会对dict的大小进行相应的扩容或者收缩。而这一过程正是通过渐进式哈希(rehash)操作来完成的。

渐进式哈希的原理:

将原哈希表中的数据以少量多次的方式,rehash到新的哈希表中,避免一次性数据迁移导致堵塞问题,通过rehashidx记录rehash的进度,在rehash结束后,新的哈希表将替代原哈希表。

在正式了解渐进式哈希之前,我们先来看几个重要的概念:

  • 负载因子:ht[0].used / ht[0].size,即哈希表的填满程度。它决定了哈希表的元素多少、空间利用率高低、哈希冲突机会的大小以及操作开销程度等,本质上是数据结构中有名的“时-空”矛盾。

  • sizemask:也叫大小掩码,用来计算索引值,其值等于哈希表size - 1。

  • rehashidx:当rehash结束时,其值为-1,rehash进行时,其值为rehash的进度,以bucket为单位,一个bucket可能有多个元素。

  • iterators:当前正在运行的安全迭代器数量。

渐进式哈希的步骤:

  • 判断负载因子,进行rehash操作。

  • 申请ht[1]的内存空间,此时dict同时拥有ht[0]和ht[1]。

    • 扩容:ht[1]的大小为大于等于ht[0].used * 2的且为2^n的值。

    • 收缩:ht[1]的大小为大于等于ht[0].used 的且为2^n的值。

  • 将dict.rehashidx置为0,开始对dict.ht[0].table[0]的bucket进行rehash。

  • 因为一个bucket是一个链表式结构,所以循环遍历这个bucket上的元素。

    • 计算每一个元素中key的新哈希值,与dict.ht[1].sizemask进行位与运算,得到索引h。

    • 根据索引h,将元素插入ht[1].table[h]。

    • 更新ht[0].used--,ht[1].used++。

    • 继续处理bucket上的下一个元素。

  • 处理完一个bucket后,将ht[0].table[dict.rehashidx] 置为 NULL。

  • 将dict.rehashidx加1,处理下一个bucket:ht[0].table[dict.rehashidx]。

  • 直到ht[0].used 为 0,说明ht[0]中的所有元素完成数据迁移。

  • 释放ht[0].table内存,将ht[1]赋值给ht[0],然后重置ht[1],为下一次rehash做准备。

  • 将dict.rehashidx置为-1,rehash工作正式结束。

渐进式哈希的控制:

/*d为需要rehash的字典,n为bucket的个数*/int dictRehash(dict *d, int n)/*在ms时间段内进行rehash操作*/int dictRehashMilliseconds(dict *d, int ms)
  • rehash操作默认是分步的,即一次只rehash一个bucket:n = 1。

  • 如果bucket为null,则继续处理下一个,但是不能超过10*n个为null的bucket:

  • 按时间段rehash,在时间段内每次rehash 100个bucket:n = 100,直到超时。

  • 连续的10*n个为null的bucket算为一个bucket。

渐进式哈希过程中访问元素:

rehash操作非一蹴而就,在rehash的过程中,ht[0]和ht[1]同时存放着数据,但是有dict.rehashidx变量标识着rehash的进度,即可以通过dict.rehashidx判断哈希值存在于ht[0]还是ht[1]。

  • 如果是新增元素,会直接操作ht[1],保证ht[0]的数据只减不增。

  • 如果dict.rehashidx值为-1,则当前没有rehash操作,直接操作ht[0]。

  • 如果dict.rehashidx值大于等于0,则表示正在进行rehash操作。

  • 将计算的ht[0]中的索引与dict.rehashidx比较,如果索引大于dict.rehashidx,表示索引还未rehash,直接操作ht[0]。

  • 如果索引不大于dict.rehashidx,则表示索引已经rehash到ht[1]。

  • 根据ht[1]重新计算索引,根据索引操作ht[1]。

  • 结束。

总结:

渐进式哈希的设计无疑是优秀的,在动态扩容收缩空间的同时,保证了Redis的服务能力,避免了阻塞;但是在rehash期间,dict同时拥有ht[0]和ht[1],申请内存空间后内存会瞬间增长,此时可能会触发Redis的过期机制或者内存淘汰策略以释放更多的内存,尤其是Redis作为lru cache长期处于maxmemory状态,势必会删除大量的key。

PS:如有任何问题或疑问,请留言告诉我。


喜欢这篇文章的朋友,欢迎关注公众号,第一时间收到更新内容。

发布了6 篇原创文章 · 获赞 2 · 访问量 121

猜你喜欢

转载自blog.csdn.net/weixin_45784328/article/details/105408993