Redis 性能优化系列 · 第 32 题

降低 Redis 内存使用的全套方法

从底层原理到实战配置,带你彻底搞懂 Redis 内存节省之道

🔍
题目说法是正确的吗?

正确,但需要补充背景。题目所说的「32 位 Redis」结合 Hash/List/ZSet/Set 存储小 KV,是一种经典的内存节省技巧。不过即便是 64 位 Redis,同样适用这个技巧——只是 32 位实例中收益更明显。

核心思路

用一个集合类型(Hash/List/ZSet/Set)存储「许多小的 Key-Value」,取代为每一个小 KV 单独建一个 Redis key。这样大幅减少了对象头开销和内存碎片。
🏗️
32 位 vs 64 位:指针大小是关键

32 位 Redis 实例

指针大小
4 字节
内存上限
~4 GB

每个对象的指针、引用占用更少字节,整体内存密度更高

64 位 Redis 实例

指针大小
8 字节
内存上限
无理论上限

支持超大内存,但每个对象多占 4 字节指针空间

32 位实例中每个指针只用 4 字节,每个 Redis 对象(robj)的固定头部本身就比 64 位小,再配合小集合的紧凑编码,省内存效果立竿见影。但代价是无法使用超过 4 GB 的内存。

💡
每个 Key 的隐藏开销:Redis 对象头

Redis 中每创建一个 key,除了存储你的数据,还会自动附加以下开销:

// Redis 内部的 redisObject 结构(简化)
typedef struct redisObject {
    unsigned type     : 4;   // 4 bit  对象类型
    unsigned encoding : 4;   // 4 bit  编码方式
    unsigned lru      : 24;  // 24 bit LRU 时钟
    int refcount;            // 4 字节 引用计数
    void *ptr;               // 8 字节 数据指针(64位)
} robj;                      // ≈ 16 字节 固定开销
      

另外还有 dict(哈希表)中的 dictEntry 结构:

typedef struct dictEntry {
    void *key;      // 8 字节
    void *val;      // 8 字节
    dictEntry *next; // 8 字节(链表指针)
} dictEntry;        // = 24 字节/条目
      

也就是说,哪怕你只存一个 1 字节的值,Redis 也要花费 至少 40~60 字节 的额外开销(对象头 + 字典条目 + SDS 字符串头)。如果有 100 万个这样的小 key,固定开销可能高达 几十 MB 甚至上百 MB

省内存的核心逻辑

把 1000 个小 KV 合并到 1 个 Hash 里,就从 1000 个 dictEntry + 1000 个 robj,变成 1 个 key 的开销 + ziplist/listpack 连续内存。内存节省 90%+ 是常见的。
ziplist / listpack:紧凑编码的秘密

当 Hash/List/ZSet/Set 中的元素数量和大小都较小时,Redis 自动使用 紧凑型连续内存编码

# Redis 7.0+ 使用 listpack 替代 ziplist(概念类似)

# 普通 hash(元素多)→ dict(散列表),内存不连续
key → dict → [dictEntry → robj → SDS] × N

# 小 hash → listpack(连续内存块)
key → listpack: [ |len|prevlen|field|len|prevlen|value| ]...
#               每个条目连续排列,无指针跳转,CPU 缓存友好
      

listpack / ziplist 的优势:

  • 所有元素存在连续内存中,无散列表结构体
  • 无额外指针开销,每个条目只有少量元数据
  • 内存分配一次完成,碎片极少
  • CPU 缓存命中率高,遍历更快

❌ 100 个独立 String key

user:1:name ~60B
user:1:age ~58B
user:1:city ~60B
× 100 用户 = ~17 KB

✅ 1 个 Hash key

hash key header ~50B
listpack entries ~6B/field
user:1 (3 fields) ~68B
× 100 用户 = ~2 KB(省 88%)
⚙️
触发紧凑编码的配置阈值

Redis 的紧凑编码在元素数量或大小超过阈值时,会自动升级为标准编码。可在 redis.conf 中调整:

# Hash:当 field 数量 ≤ 128 且每个 field/value ≤ 64 字节时用 listpack
hash-max-listpack-entries 128
hash-max-listpack-value   64

# List:小列表使用 listpack
list-max-listpack-size    128

# ZSet:有序集合小集合使用 listpack
zset-max-listpack-entries 128
zset-max-listpack-value   64

# Set:整数集合优化(元素全为整数时)
set-max-intset-entries    512

# Set:listpack 编码(Redis 7.2+)
set-max-listpack-entries  128
set-max-listpack-value    64
      

点击每个方法卡片可展开/折叠详细说明。以下 8 种方法覆盖了生产环境中最主流的 Redis 内存优化手段。

📋
8 大方法汇总对比表
# 方法 节省效果 实施难度 适用场景
1 Hash 存储小 KV ★★★★★ ~90%
大量结构化小对象
2 设置 TTL 过期时间 ★★★★☆ 视情况
所有缓存场景
3 maxmemory + 淘汰策略 ★★★☆☆ 中等
所有场景(必须配置)
4 压缩 value(gzip等) ★★★★☆ ~60%
大 value(>500B)
5 二进制序列化格式 ★★★☆☆ ~40%
结构化对象存储
6 开启碎片整理 ★★☆☆☆ ~20%
频繁增删 key 的场景
7 冷热分离 ★★★★☆ 显著
数据量大、访问冷热不均
8 HyperLogLog/Bitmap/BloomFilter ★★★★★ 95%+
统计、去重、判存在
🎯
实战优化清单(按优先级)
  1. 第一步:检查所有 key 是否设置了合理的 TTL → redis-cli --scan
  2. 第二步:配置 maxmemoryallkeys-lru 策略
  3. 第三步:用 redis-cli --bigkeys 找出大 key 并拆分
  4. 第四步:将小 KV 合并到 Hash,控制 field 数量在 128 以内
  5. 第五步:大 value 开启压缩(gzip/snappy)
  6. 第六步:开启 activedefrag yes 自动碎片整理
  7. 第七步:评估冷热分离,历史数据迁移至关系型数据库
  8. 第八步:统计/去重场景替换为 HyperLogLog/Bitmap
💡

监控内存的核心命令

随时运行 INFO memory 查看 used_memory_human(实际使用)、mem_fragmentation_ratio(碎片率); 运行 MEMORY DOCTOR 可获得 Redis 自动诊断建议; 使用 OBJECT ENCODING key_name 查看某个 key 的实际编码方式,确认是否命中紧凑编码。