redis hsacn时候遇到rehash会不会取到重复数据

2,762 阅读4分钟

1、首先redis 的hash结构底层实现是字典

2、字典扩容和缩容是会发生rehash

3、rehash是渐进式的

4、hscan 取数据时,如果正发生rehash,会取到重复数据吗?


我们先看第一个问题,回忆下字典的结构:

一般hash表存在ht[0], 在rehash 的时候会把数据向ht[1]转移,结束后ht[0] = ht[1],释放掉ht[1]


我们再回忆一下 字典发生rehash的的三个契机:

 哈希表的负载因子 

 # 负载因子 = 哈希表已保存节点数量 / 哈希表大小

load_factor = ht[0].used / ht[0].size

1) bgsave 正在发生,且 load_factor > 5

2) basave 未发生,load_factor > 1

3) 缩容 load_factor < 0.1  


我们再看下 字典 渐进式rehash 的全过程

  1. 为字典的备用哈希表分配空间

  2. 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2n 次方幂);如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。 这里我理解,n应该指次数,比如第一次rehash n就是1, 已用节点数 是 3 如果是扩容,那么ht[1] 的容量就是 (3* 2)* 2  ,缩容的话就是3 * 2

  3. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始(为-1时表示没有进行rehash)。

  4. rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当一次rehash工作完成之后,程序将rehashidx属性的值+1。同时在serverCron中调用rehash相关函数,在1ms的时间内,进行rehash处理,每次仅处理少量的转移任务(100个元素)。

  5. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。


那么回到我们核心的问题,hscan 时候,遇到rehash,究竟会不会取到 相同元素呢?

我们分三种情况考虑

1、从迭代开始到结束,哈希表没有进行rehash

2、从迭代开始到结束,哈希表进行了rehash,但是每次迭代时,哈希表要么没开始rehash,要么已经结束了rehash

3、从迭代开始到结束,某次或某几次迭代时哈希表正在进行rehash

第一种情况 比较简单, hscan 扫描时,没有进行rehash ,正常返回游标结果。

第二种情况比较复杂。假设redis的哈希表大小为4,如果rehash完后size变成了8.

 按照公式  hash(key) & (size - 1) 的话,即如果size是4时,hash(key)&11,如果size是8时,hash(key)&111.因此当从4扩容到8时,原先在0bucket的数据会分散到0(000)与4(100)两个bucket。原先1bucket 会分散到 1 和 5, 依次类推,当还是按照 原先的hash方法取数据的话,从bucket4 开始取到的都是重复数据。

第三种情况,如果返回游标1时正在进行rehash,ht[0]中的bucket 1中的部分数据可能已经rehash到 ht[1]中的bucket[1]或者bucket[5],此时必须将ht[0]和ht[1]中的相应bucket全部遍历,否则可能会有遗漏数据

分析完三种情况,如果想统一解决,必须要采取特别的措施了

redis大叔们,发明了一种reverse binary iteration的方法,具体的游标算法很简单:



备注,上面的公式是计算下一个游标的方法。比如第一个游标0,下一个计算后是2,2再计算下一个是 1,1再计算下一个是3,3再计算下一个是0

遍历size为4时的游标状态转移为0-2-1-3.

同理,size为8时的游标状态转移为0-4-2-6-1-5-3-7.

size为16时的游标状态转义为0-8-4-12-2-10-6-14-1-9-5-13-3-11-7-15

可以看出,当size由小变大时,所有原来的游标都能在大的hashTable中找到相应的位置,并且顺序一致,不会重复读取并且不会遗漏

但是当size由16变为了4 的时候,但如果游标返回的不是这四(0,2,1,3),例如返回了10,10&11之后变为了2,所以会从2开始继续遍历.但由于size为16时的bucket2已经读取过,并且2,10,6,14都会rehash到size为4的bucket2,所以会造成重复读取

总结:redis里边rehash从小到大时,scan系列命令不会重复也不会遗漏.而从大到小时,有可能会造成重复但不会遗漏.