redis hash在field数超512或任一value长度超64字节时,由ziplist不可逆切换为hashtable;随后当负载因子≥1(或强制阈值>5)触发渐进式rehash,内存峰值可达原占用2.5倍。

Hash 扩容本身不阻塞主线程,但会显著增加内存占用和 CPU 开销,尤其在 big hash 场景下容易引发延迟毛刺甚至 OOM。
hash 什么时候从 ziplist 切换到 hashtable
Redis 不是“按需扩容”,而是先触发编码转换,再走 dict 的 rehash 流程。这个切换点直接决定后续性能走向:
- 默认条件:所有
field和value长度 ≤64 字节,且总数量 ≤512 —— 满足则用ziplist;任一不满足,立刻转为hashtable - 配置项
hash-max-ziplist-entries和hash-max-ziplist-value可调,但修改后只对新建 hash 生效,已有 big hash 不会回退 - 一旦转成
hashtable,就不可逆 —— 即使后续删到只剩 10 个 field,底层仍是 dict 结构 -
ziplist查找是 O(n),但内存紧凑;hashtable平均 O(1),但每个 entry 多出指针、bucket 数组等开销
hashtable 扩容触发的两个阈值很关键
不是“数据一多就扩”,而是由两个硬性条件控制,生产环境常因忽略后者踩坑:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 常规扩容:当
ht[0].used / ht[0].size ≥ 1时触发,目标 size =ht[0].used * 2 - 强制扩容:即使
dict_can_resize = 0(比如运维禁用了自动 resize),只要负载因子 >dict_force_resize_ratio(默认为 5),仍会扩 —— 这是防爆的最后一道闸 - 扩容后立即启用渐进式 rehash,但
rehashidx从 -1 变为 0,意味着迁移开始;此时每次HGET/HSET都会顺带搬一个 bucket(可能含多个节点) - 注意:
ht[1]分配内存时就占用了新空间,旧表ht[0]还没释放 —— 内存峰值 ≈ 原占用 × 2.5 倍
big hash 导致 rehash 时间拉长、CPU 毛刺明显
单个 hash 存几十万 field 时,rehash 不再“平滑”,实际表现接近半阻塞:
- 每个 bucket 迁移耗时取决于链长;若某个 bucket 聚集了上千个 key(哈希冲突高),一次操作可能卡住几毫秒
- 客户端观察到的现象:
HGETALL响应时间从 0.2ms 突增至 8–12ms,且抖动剧烈;INFO stats中instantaneous_ops_per_sec瞬间跌落 - Redis 日志里不会报错,但
redis-cli --stat可看到rehashing字段持续非零,且used_memory_human快速上涨 - 监控重点不是 “是否在 rehash”,而是
mem_fragmentation_ratio> 1.5 +rehashing> 0 同时出现 —— 表明内存压力与迁移正在叠加
线上避免 Hash 扩容冲击的实操建议
别等它自己扩,得从设计和配置上掐住源头:
- 写入前预估规模:单个 hash 的 field 数超 1000 就该警惕;超 5000 基本要拆分,比如按时间分片
user:1001:profile:2026Q2 - 禁用自动 resize 仅治标,真正有效的是限制单 hash 容量 —— 用
HEXISTS+HLEN在业务层做守门员,超阈值主动拒绝或告警 - 不要迷信
hash-max-ziplist-entries 1000:设太高会让 ziplist 查找变慢,反而放大 CPU 使用率;保持默认 512 更稳妥 - 紧急情况下可手动干预:执行
DEBUG REHASHING查看进度,但无法暂停;真卡死只能 failover 或临时扩容机器扛住内存峰值
最易被忽略的一点:hash 编码切换和 dict rehash 是两套机制,前者影响内存布局,后者影响运行时性能,但日志里都只显示 “rehashing”,得靠 OBJECT ENCODING 和 INFO memory 组合判断真实瓶颈在哪。










