Redis 渐进式 rehash 是什么?为什么不能一次性扩容?
简化版
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 dict | Java HashMap |
|---|---|---|
| 服务模型 | 单线程事件循环 | 应用内对象 |
| 扩容目标 | 控制线上延迟 | 保持结构效率 |
| 迁移方式 | 渐进式迁移 | 常见为一次性迁移 |
| 查询期间 | 可能查两张表 | 通常迁完后再用新表 |
这个对比能帮助面试官看到你理解了场景差异,而不是机械背概念。
八、常见误区与追问
- 误区:渐进式 rehash 会减少总迁移成本。 总成本没有消失,只是被分摊到多次操作中。
- 误区:rehash 期间只查新表。 旧表还有未迁移桶,查询必须兼顾旧表和新表。
- 误区:Redis 不会因为扩容产生延迟。 渐进式降低尖刺,但大 key、大量写入、内存紧张仍可能抖动。
- 追问:新增元素为什么放新表? 避免旧表继续增长,加快旧表清空。
- 追问:如果没有请求,rehash 会不会停住? Redis 可通过周期任务推进部分迁移,但主要仍随字典操作推进。
- 追问:渐进式 rehash 和 BigKey 有什么关系? BigKey 内部结构过大时,迁移、释放、复制都会放大延迟风险。
九、加强记忆
记住“开新表、留旧表、搬一点、查两边”。渐进式 rehash 的本质是用短时间的额外复杂度换取长时间的延迟平滑。
面试时先说明为什么扩容,再讲一次性扩容的风险,最后讲双表迁移和代价,答案就完整了。