Redis 内存优化全攻略
一、为什么 Redis「吃」内存?
- Redis 所有数据常驻内存,每个对象不仅包含值本身,还包含类型、编码方式、引用计数等元数据。
- 默认使用
jemalloc做内存分配;对象频繁创建 / 释放会导致 内存碎片,RSS(常驻集大小)与used_memory差距拉大。 - 了解并善用 Redis 提供的多种“紧凑编码”与位级操作,可以节省 3~10 倍空间。([redis.io][1])
二、特殊编码:让小集合“瘦身”
| 数据类型 | Redis <= 6.2 | Redis 7.0+ | 触发条件(元素数 / 元素长度) |
|---|---|---|---|
| Hash | ziplist | listpack | hash-max-listpack-entries / hash-max-listpack-value(默认 512 / 64) |
| Set | intset(全整数)→ listpack(7.2 新增) | set-max-intset-entries / set-max-listpack-entries | |
| ZSet | ziplist → listpack | zset-max-listpack-entries / ...-value |
- 编码在小集合上自动生效,透明对业务;超过阈值即回退到哈希表 / 跳表。
- 可通过
OBJECT ENCODING <key>查看某键当前编码。([redis.io][1], [redis.io][2])
调参建议
# redis.conf
hash-max-listpack-entries 1024 # 如果字段很多但都很短,可适度提高
hash-max-listpack-value 128 # 值长度一般 < 128B
zset-max-listpack-entries 256
⚠️ 把阈值调得过大,会导致“胖集合→哈希表”转换耗时变长;上线前务必压测。
三、32 位实例:极致省内存的“小金刚”
make 32bit编译可把指针宽度从 8 B 降到 4 B,每个键平均节省 16~24 B。- 进程地址空间仅 4 GB,适合嵌入式 or 高密度容器场景。RDB/AOF 与 64 位实例兼容,可随时切换。
四、位 / 字节级操作:1 亿用户也只要 12 MB
SETBIT maillist:user 10000000 1 # 为第 10000000 位打标
BITCOUNT maillist:user # 统计总订阅数
- 位图方案非常节省空间:1 bit/用户,1 亿用户 ≈ 12 MB。
GETRANGE / SETRANGE可把字符串当随机访问字节数组,用于稀疏计数器、布隆过滤器等。([redis.io][1])
五、用 Hash 模拟超轻量 KV
理念:与其创建 1 万个顶级 key,不如用“一个 Hash + 1 万个 field”。
5.1 普通对象与 Hash 的对比
# 方案 A:五个独立键
SET user:1:name "Alice"
SET user:1:mail "a@example.com"
...# 方案 B:一个 Hash
HSET user:1 name "Alice" mail "a@example.com"
在默认阈值下,10 个以内字段的 Hash 会使用 listpack/ziplist,元数据仅 ~4 B/field,远小于顶级 key 的几十字节。
5.2 “两级切片”技巧,10× 压缩普通 KV
当 Key 命名规则为 object:<id>、<id> 为自增数字时,可把 后两位作为 field,其余作为 Hash key:
def split(key) # object:1234head, tail = key.split(':'){ hash: "#{head}:#{tail[0..-3]}", field: tail[-2..] } # object:12 , 34
end
- 每个 Hash 固定 100 个 field(00~99),充分利用 listpack。
- 作者实测 10 万条记录仅占 1.7 MB,而直接
SET需要 11 MB。([redis.io][1])
六、maxmemory 与碎片治理
-
务必配置
maxmemory,否则 Redis 会一路吃光机器内存。 -
当键删除后,
used_memory下降但 RSS 不降:- 归因于
jemalloc不能把已分配的大页立即归还 OS。 - 下一次写入会优先复用空闲块,不再增加 RSS。
- 归因于
-
监控指标
used_memory_rss / used_memory→ 碎片率 > 1.5 时需关注。- Linux
smem -r或pss观测真实占用。
-
定期重启(AOF 重写或
MEMORY PURGE)可释放碎片;生产 7×24 高可用场景请结合主从滚动重启。([redis.io][1])
七、一步步落地内存优化
| 步骤 | 命令 / 工具 | 目的 |
|---|---|---|
| ① 找大键 | redis-cli --bigkeys、redis-cli --memkeys | 锁定“内存大户” |
| ② 看编码 | OBJECT ENCODING k | 是否仍是 hashtable / skiplist |
| ③ 升级小集合 | 调低 ...-listpack-entries | 让更多键触发紧凑编码 |
| ④ 改数据模型 | Hash 聚合、小数值存 bitmap | 降维打击 |
| ⑤ 校验结果 | INFO memory、MEMORY STATS | 对比优化前后 |
八、生产最佳实践清单
- 启用持久化(AOF everysec + RDB snapshot),防止
maxmemory-policy逐出后找不到热数据。 - 大批量插入前先
CONFIG SET hash-max-listpack-entries心里有数;避免上线后阈值调高导致“批量解压”卡主线程。 - 禁止 Lua/事务一次访问超大 Hash,否则会触发编码升级 + 阻塞。
- 倾向选择
allkeys-lru/allkeys-lfu,并监控逐出速率;写 OOM 比业务慢更糟糕。 - 碎片率持续 > 2 时,排查是否有“大键写入→删除”尖峰;必要时做冷重启。
九、总结
Redis 省内存 = 结构压缩 + 模型重构 + 参数调优 + 监控治理
- 利用 listpack/intset 让“小集合占小钱”。
- Hash 可以当“迷你数据库”用,合理拆分 key 能省一个数量级。
- 位图 + 字节数组是处理超大布尔/计数集的王炸。
- 控好
maxmemory、理解 jemalloc 行为,就不会被碎片“坑死”。
跟着本文逐条实践,你可以在 同配置机器上多存 3~10 倍数据,或在预算不变的情况下减轻节点压力、推迟扩容时机。祝各位内存一步到位,弹性再也不是难题!