← 返回题目列表

Redis 渐进式 rehash 是什么?为什么不能一次性扩容?

高频 中等 第 9 / 36 题 更新于 2026/07/29
Redisrehash哈希表扩容

简化版

Redis 字典扩容或缩容时不会一次性把所有键搬完,而是同时维护两个哈希表,把迁移工作分摊到后续增删改查过程中,这叫渐进式 rehash。这样可以避免一次性迁移大量 key 阻塞主线程。

详细版

Redis 很多结构都依赖 dict,例如 keyspace、Hash、Set、ZSet 的 member 索引。哈希表负载因子变化后需要扩容或缩容。

如果一次性 rehash 100 万个 key,主线程会长时间阻塞,所有客户端请求都排队。渐进式 rehash 的做法是:

  • 分配新哈希表 ht[1]
  • 保留旧哈希表 ht[0]
  • 设置 rehash 索引。
  • 每次字典操作顺手搬迁一部分桶。
  • 查询时同时查新旧两个表。
  • 搬完后释放旧表。

它牺牲了一段时间内的双表查询和实现复杂度,换来延迟稳定性。

完整版教学

一、为什么哈希表需要 rehash

哈希表通过桶数组存放元素。元素越来越多时,冲突会增多,链表或节点查找成本上升;元素变少时,桶数组太大又浪费内存。所以需要扩容或缩容。

假设桶数量是 1024,元素数量是 5000,平均每个桶接近 5 个元素,冲突明显变多。扩容到 8192 个桶后,平均每桶约 0.61 个元素,查找会更稳定。

记忆钩子:rehash 的目标不是“换个表”,而是把负载因子拉回合理区间。

二、一次性 rehash 为什么危险

Redis 主线程执行命令。如果在某次写入中触发扩容,并一次性搬迁 100 万个 key,这次命令就可能耗时几十毫秒甚至更久。对缓存系统来说,几十毫秒的阻塞会让大量请求排队。

一次性 rehash:
触发扩容 -> 搬迁全部桶 -> 主线程阻塞 -> 后续请求排队

渐进式 rehash:
触发扩容 -> 每次搬一点 -> 延迟摊平

所以渐进式 rehash 的核心价值是平滑延迟,而不是减少总工作量。总搬迁工作仍然存在,只是被分散了。

三、Redis 的双哈希表设计

Redis 字典通常有两个哈希表:ht[0]ht[1]。平时只用 ht[0];开始 rehash 时,给 ht[1] 分配新容量,然后逐步把 ht[0] 的桶迁移过去。

rehash 中:
ht[0]: 旧表,部分桶未迁移
ht[1]: 新表,新写入直接进入这里
rehashidx: 当前迁移到哪个桶

这样做会让一段时间内结构更复杂,但每次迁移只处理少量桶,能控制单次命令延迟。

四、查询、写入、删除期间怎么处理

rehash 期间,查询要先查 ht[0],再查 ht[1],或者根据实现顺序同时考虑两张表。新增元素通常直接写入 ht[1],避免旧表继续膨胀。删除则需要两张表都考虑。

操作rehash 期间行为
查找可能查旧表和新表
新增一般放入新表
删除两张表都要处理
迁移每次操作顺带搬若干桶

这个过程解释了为什么 rehash 期间某些操作会有一点额外成本,但相比一次性停顿更可控。

五、渐进式 rehash 的时间线

可以把它理解成一次搬家:先租好新房子,然后每天搬几箱,而不是一天把所有家具搬完。

T0 负载因子过高
T1 分配 ht[1]
T2 rehashidx = 0
T3 每次命令迁移若干桶
T4 ht[0] 全部迁完
T5 ht[1] 变成新的 ht[0]

如果 Redis 一段时间没有请求,也可能在定时任务里推进部分 rehash,避免一直停在中间状态。

六、渐进式 rehash 的代价

渐进式 rehash 不是免费午餐。它会临时持有两张表,占用更多内存;查询路径也可能变成双表查找;实现上还要处理迁移进度和并发语义。

假设旧表 1 GB,新表扩容后可能需要额外内存。虽然不是所有数据复制一份,但桶数组和节点迁移期间的峰值内存压力要考虑。在线上内存紧张时,扩容可能引发更明显的抖动。

所以 Redis 仍然建议避免单个 Hash/Set 过大,也要合理设置内存上限和监控内存碎片。

七、和 Java HashMap 扩容怎么对比

Java HashMap 在普通场景里扩容通常由某次 put 触发,并在当次操作中迁移数组;Redis 由于是服务端单线程处理请求,更关注尾延迟,所以采用渐进式策略。

对比项Redis dictJava HashMap
服务模型单线程事件循环应用内对象
扩容目标控制线上延迟保持结构效率
迁移方式渐进式迁移常见为一次性迁移
查询期间可能查两张表通常迁完后再用新表

这个对比能帮助面试官看到你理解了场景差异,而不是机械背概念。

八、常见误区与追问

  • 误区:渐进式 rehash 会减少总迁移成本。 总成本没有消失,只是被分摊到多次操作中。
  • 误区:rehash 期间只查新表。 旧表还有未迁移桶,查询必须兼顾旧表和新表。
  • 误区:Redis 不会因为扩容产生延迟。 渐进式降低尖刺,但大 key、大量写入、内存紧张仍可能抖动。
  • 追问:新增元素为什么放新表? 避免旧表继续增长,加快旧表清空。
  • 追问:如果没有请求,rehash 会不会停住? Redis 可通过周期任务推进部分迁移,但主要仍随字典操作推进。
  • 追问:渐进式 rehash 和 BigKey 有什么关系? BigKey 内部结构过大时,迁移、释放、复制都会放大延迟风险。

九、加强记忆

记住“开新表、留旧表、搬一点、查两边”。渐进式 rehash 的本质是用短时间的额外复杂度换取长时间的延迟平滑。

面试时先说明为什么扩容,再讲一次性扩容的风险,最后讲双表迁移和代价,答案就完整了。