这是我参与更文挑战的第3天,活动详情查看: 更文挑战
概述
前两天整理了redis中字典的实现==》字典终极篇。强烈建议没看的XDM可以先去看一遍(看一遍 看一遍 看一遍 重要的事情说三遍😄),字典底层使用到哈希表,字典终极篇 这里也提到当哈希表需要扩展或者收缩的时候会将我们ht[0]里面的数据rehash到ht[1]中,而这个过程并不是一下完成的,而是一个多次 渐进的过程。好优雅的设计 崇拜~
渐进式的原因
分多次进行的原因是,如果我们的ht[0]中保存的键值对是十几或者几百这种较小的值,我们一次rehash是没有问题的,但是如果是几十万几百万呢?一次想要把这些key value rehash到ht[1]是需要一定的时间的,redis服务短时间不可用。这当然是不允许的哈~(redis不可用,那我们的服务请求不就都打到mysql了 导致mysql服务被打挂 然后我们的服务直接全是500 哇!不敢想象。。)
文字描述rehash的过程
- 根据字典终极篇中我们提到的ht[1]分配策略进行内存分配,此时同时存在ht[0]和ht[1]两个哈希表。
- 这个时候将我们字典结构上的rehashidx设置为0表示我们的rehash正式开始(之前是-1)。
- 在rehash进行期间,定时任务会每次都会对ht[0]上的100 * n个key(这个为什么是100*n呢?下面看下源码xdm就知道了),我们对字典的增删改查会进行一个非空key的rehash。每完成一个key都会将rehashidx的值加1
- 随着字典操作的不断进行 ht[0]上的所有键值都rehash到ht[1]后 ht[0]是一个空的哈希表。将ht[0]指向ht[1],ht[1]指向null,这个时候rehashidx设置成-1 表示rehash完成。
渐进式rehash的好处是采用了分而治之的方法,将rehash的过程分散到每个对字典的操作中去,避免了一起rehash庞大的计算量。
不多叨叨,来看下源码
我们来看下这个dictRehashMilliseconds方法,这个方法会在redis的定时任务中databasesCron进行调用的:
1 | ini复制代码int dictRehashMilliseconds(dict *d, int ms) { |
在这个地方有一个死循环,只有时间超时才会break,也就是说单次的rehash每次最多是100ms,但是xdm有没有发现一个bug,就是如果dictRehash(d,100)这个方法执行超过100ms 这次就会超过100ms了。(redis其实自己是默认每次哈希100个key的时间是小于100ms的),这个地方就是上边说的为什么是100*n个key了
1 | kotlin复制代码 while(dictRehash(d,100)) { |
接下来我们看一下最核心的dictRehash方法,这个就是核心的rehash方法。empty_visits也是一个非常非常优雅的设计,如果累计访问的null值是我们要rehash数的10倍就直接return,也是为了防止我们哈希表中null很多的情况下rehash过程花费太多的时间(这个设计真的优雅到我了)
我们在对字典进行操作的时候也会对字典进行dictRehash的操作,不过这种只会操作一个非空的key。
最后这个地方的逻辑就是rehash完成的实现:
1 | ini复制代码if (d->ht[0].used == 0) { |
我们可以发现
rehashidx的作用很重要,他会记录我们当前dict的rehash所进行到的索引值(我们可以分治的重要字段),也是我们当前dict是否在rehash的判断标志。
总结
当我们在rehash过程中对字典进行增删改查操作会是什么情况呢?
- 更新:先查询ht[0]上有没有如果有则更新,如果没有则去ht[1]上进行查询更新(查询、删除也是这个逻辑)。
- 新增:新增的话会直接操作ht[1],这样可以保证ht[0]上不会再新增了。
xdm redis的rehash到这就完成了,设计的真的是相当的优雅,我们可以将这种思想运用到我们的日常设计开发中,让我们的代码都优雅起来~
本文转载自: 掘金