Skip to content

06.1 - 内存优化策略

定位: Redis 数据在内存中,合理优化内存是生产环境的必修课 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 淘汰策略选择、内存碎片成因、Big key 危害、编码优化


一、内存淘汰策略

当内存使用达到 maxmemory 时,Redis 触发淘汰策略:

策略行为
noeviction不淘汰 → 写入返回 OOM 错误
allkeys-lru ★从所有 key 中按 LRU 淘汰
volatile-lru从设置了过期的 key 中淘汰
allkeys-random随机淘汰
volatile-ttl淘汰即将过期的 key
volatile-random从有过期的 key 中随机
allkeys-lfu ★从所有 key 中按 LFU 淘汰
volatile-lfu从有过期的 key 中按 LFU 淘汰

推荐配置:allkeys-lru(大多数缓存场景);volatile-lfu(数据分级——重要 key 设过期,不重要 key 自然淘汰)

LRU vs LFU

  • LRU:最近最少使用(关注"多久没用了")
  • LFU:最不经常使用(关注"用了多少次")
  • 突发热点 → LRU 更合适(刚热起来不会被淘汰)
  • 稳定访问 → LFU 更合适(低频率 key 被淘汰)

二、内存碎片

> **碎片产生原因**:
> 1. **内存分配器(jemalloc)特性**:固定大小分配(8/16/32/.../4KB),17 字节 → 分配 32 字节 → 15 字节碎片
> 2. **数据频繁增减**:SET A (1KB) → DEL A → SET B (2KB),释放的空间不连续 → 新数据放不下
> 3. **过期 key 删除**:大量 key 过期 → 释放碎片 → RF 升高

监控碎片率:
  127.0.0.1:6379> INFO memory
  used_memory_rss_human: 2.1G   # 实际物理内存占用
  used_memory_human: 1.5G       # 实际数据占用
  mem_fragmentation_ratio: 1.4  # 碎片率 = RSS/used

碎片率解读:
  ≈ 1.0:正常(RSS ≈ used)
  < 1.0:发生了 swap(危险!内存用超了)
  1.0-1.5:正常范围
  > 1.5:碎片较多,需要清理

清理碎片(Redis 4.0+):
  # 自动碎片清理
  CONFIG SET activedefrag yes
  CONFIG SET active-defrag-threshold-lower 10  # 碎片超过 10% 开始
  CONFIG SET active-defrag-threshold-upper 100 # 超过 100% 全力清理
  
  # 查看清理效果
  INFO memory | grep defrag

三、Big Key 问题

> **Big Key 的危害**:
> 1. **阻塞服务**:1 个 10MB 的 String → GET bigkey 耗时 10-50ms → 单线程事件循环被阻塞 → 所有请求排队等待
> 2. **内存不均**:集群中一个 key 占 1GB → 分布不均匀 → 一个节点内存快满,其他节点还有大量空间
> 3. **网络带宽**:GET 1MB key → 1MB 数据通过网络传输;1000 QPS → 1GB/s 网络占用

哪些是 Big Key?
  String:> 10KB
  List:元素超过 5000
  Hash:元素超过 5000 或 value > 10KB
  Set/ZSet:元素超过 5000

查找 Big Key:
  redis-cli --bigkeys        # 扫描统计大 key
  redis-cli --memkeys        # 按内存排序
  DEBUG SDSLEN <key>         # 查看具体 key 大小
  MEMORY USAGE <key>         # 显示 key 的内存使用

处理 Big Key:
  ✓ 拆分:hash = field1, field2, ... 替代大 String
  ✓ 压缩:客户端压缩后存储(Gzip/Zstd)
  ✓ 分批:对大 list/set 用 LRANGE/SCAN 分批处理
  ✓ 预警:设置最大内存限制 + 监控

四、编码优化

不同数据类型的内存优化:

Hash 优化(推荐小对象用 ziplist):
  hash-max-ziplist-entries 512
  hash-max-ziplist-value 64
  → 小数据用 ziplist(省 ~70% 内存)
  → 大数据 dict(高性能)

ZSet 优化:
  zset-max-ziplist-entries 128
  zset-max-ziplist-value 64

Set 优化:
  set-max-intset-entries 512
  → 全整数时用 intset

内存占用对比:
  数据类型    优化前              优化后
  ────────    ──────              ──────
  10万个小Hash  dict: 150MB       ziplist: 30MB  ↓80%
  100万整型Set  hashtable: 70MB   intset: 15MB   ↓79%
  100万 ZSet   skiplist: 120MB   ziplist: 40MB  ↓67%

五、实战验证

5.1 查看淘汰策略

bash
redis-cli

# 查看当前策略
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

# 修改为 allkeys-lru
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK

# 查看内存限制
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "4294967296"  # 4GB

5.2 查看内存碎片

bash
redis-cli

127.0.0.1:6379> INFO memory | grep -E "used_memory|fragmentation"
# used_memory: 1573741824       # 实际数据 1.5GB
# used_memory_rss: 2201235456   # RSS 2.1GB
# mem_fragmentation_ratio: 1.40  # 碎片率 1.4

# 启用碎片清理
127.0.0.1:6379> CONFIG SET activedefrag yes
OK

5.3 查找大 Key

bash
# 扫描大 key(生产低峰期执行)
redis-cli --bigkeys

# 输出示例:
# Biggest string found so far '"bigkey:1"' with 5242880 bytes
# Biggest hash  found so far '"users"' with 10234 fields
# Biggest set   found so far '"tags"' with 50000 members

# 用 MEMORY USAGE 查看具体 key
redis-cli MEMORY USAGE "bigkey:1"  # 字节

# 用 DEBUG 查看 string 大小
redis-cli STRLEN "bigkey:1"

5.4 内存基准

bash
# 查看不同编码的内存差异
# 1000个 Hash 对象(每个 5 个字段)

# 小值(ziplist 编码)
for i in $(seq 1 1000); do
  redis-cli HSET "small:hash:$i" name "user$i" age "$i" > /dev/null
done
redis-cli INFO memory | grep used_memory_human

# 大值(超过 64 字节)
for i in $(seq 1 1000); do
  redis-cli HSET "big:hash:$i" name "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" age "$i" > /dev/null
done
redis-cli INFO memory | grep used_memory_human
# 观察内存差异(新创建 1000 个大值导致 hashtable 编码)

六、面试视角

追问答案要点
内存淘汰策略怎么选?缓存场景 allkeys-lru/LFU;数据库场景用 noeviction(避免丢数据)
mem_fragmentation_ratio < 1 意味着什么?内存发生 swap(危险!),说明物理内存不够,Redis 使用了 swap 分区
碎片清理(activedefrag)的原理?Redis 4.0+ 自动整理碎片,通过搬移数据合并空闲块,主线程和后台线程协作
Big key 怎么发现?redis-cli --bigkeys 扫描;MONITOR 记录大响应;定期 scan + STRLEN/HLEN 抽样
1GB 的文件存 Redis 怎么存?不要存!Redis 不适合存大文件。拆分或换其他方案(对象存储 S3/MinIO)
为什么 ziplist 能省内存?连续内存无指针开销、无哈希表浪费、编码紧凑(如小整数用 2 字节存)

📚 相关链接

  • **SDS** — SDS 的惰性释放和编码转换
  • **Hash 编码** — ziplist→hashtable 的优化配置
  • ← 返回 **性能优化与运维索引**

Knowledge4J — Java 知识库