redis 的渐进式 rehash,为什么 java 不采用渐进式 rehash
说明
- @author JellyfishMIX - github / blog.jellyfishmix.com
- LICENSE GPL-2.0
- 本文默认已了解 jdk 1.8 HashMap 的 rehash 机制。
redis 的 dict
- redis 的 dict 是一个用于维护 key 和 value 映射关系的数据结构,与很多语言中的 Map 或 Dictionary 类似。redis 的一个 database 中所有 key 到 value 的映射,就是用一个 dict 来维护的。dict 本质上是为了解决算法中的查找问题(search)。
- 一般查找问题的解法分为两个大类:一个是基于各种平衡树,一个是基于哈希表,平常使用的各种 Map 或 Dictionary,大多是基于哈希表实现的。当然 jdk 1.8 中 HashMap 使用了哈希表 + 链表/红黑树的组合进行了优化,但本质上还是哈希表。
- redis 的 dict 也是一个基于哈希表的数据结构。
redis 渐进式 rehash
-
redis dict 存在两个数组 ht[0], ht[1]。扩容和收缩的时候,如果 dict 中有很多元素,一次性将这些键从 ht[0] 全部 rehash 到 ht[1] 的话,期间一直在处理 rehash 的工作,可能会导致 dict 较长时间不可用,无法对外提供服务。所以,redis 采用渐进式 rehash 的方式。
-
在字典中维持一个索引计数器变量 rehashidx,它的值为 -1 表示 rehash 没有开始。将 rehashidx 的值设置为0,表示 rehash 工作开始。每次的增删改查,rehashidx + 1,然后执行原 hash 表 rehashidx 索引位置的 rehash。
-
这样 rehash 的消耗分摊在每一次对 dict 的访问中,避免集中式 rehash 带来的庞大计算量导致服务一段时间内不可用。
java 不采用渐进式 rehash 的原因
渐进式 rehash 看起来很好,为什么 java 的 HashMap 不采用这样渐进式 rehash?
时间开销
- HashMap 一般作为局部变量用的多,即生命周期是仅在一次方法的调用中。如果是 web 应用,很多 HashMap 的生命周期仅在本次请求内。
- 渐进式 rehash 比集中式 rehash 整体上多了额外的计算开销。这样渐进式 hash 会让作为局部变量的 HashMap rehash 总耗时增加,进而导致本次方法调用耗时增加。
- HashMap 用作局部变量的场景是最多的,这种情况下,渐进式 rehash 耗时都是这这一次方法调用内。如果是 web 应用,局部变量的渐进式 rehash 耗时会叠加在本次请求内。由于渐进式 rehash 带来的额外计算开销,渐进式 reahsh 会比集中式 rehash 整体耗时长。没有优化,反而是一种负担。
- 虽然一些框架把 HashMap 用作共享变量,生命周期可能会像 redis 的 dict 一样,在整个应用的生命周期中。但是 HashMap 用到最多的地方还是局部变量,不能为了少量框架里的 HashMap,被迫让局部变量的 HashMap 变成渐进式 rehash。
空间开销
空间开销上,渐进式 rehash 可预见性地增加了很多内存消耗,因为很长一段时间内要维护两个数组。
总结
渐进式 rehash 只适合用在作为共享变量的哈希表中,且多次方法调用(特别是多次请求)都会去操作这个共享变量。这样才能发挥其 rehash 耗时每次调用均分的优势。比如 redis 的 dict 就是很好的例子。