从底层原理到实战配置,带你彻底搞懂 Redis 内存节省之道
正确,但需要补充背景。题目所说的「32 位 Redis」结合 Hash/List/ZSet/Set 存储小 KV,是一种经典的内存节省技巧。不过即便是 64 位 Redis,同样适用这个技巧——只是 32 位实例中收益更明显。
每个对象的指针、引用占用更少字节,整体内存密度更高
支持超大内存,但每个对象多占 4 字节指针空间
32 位实例中每个指针只用 4 字节,每个 Redis 对象(robj)的固定头部本身就比 64 位小,再配合小集合的紧凑编码,省内存效果立竿见影。但代价是无法使用超过 4 GB 的内存。
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。
当 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 的优势:
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 内存优化手段。
将大量小 key 合并到 Hash 中,利用 listpack 紧凑编码,节省 50%~90% 内存。
# ❌ 低效:每个属性一个 key SET user:1001:name "张三" SET user:1001:age "25" SET user:1001:score "98.5" # ✅ 高效:Hash 存储同一实体的多个属性 HSET user:1001 name "张三" age "25" score "98.5" # ✅ 进阶:Hash 分片(将百万 key 分成 1000 个 Hash) # key = "user:" + (userId / 100),field = userId % 100 HSET user:10 01 "张三" # userId=1001 HSET user:10 02 "李四" # userId=1002
不设置 TTL 的 key 会永久占用内存。对于缓存类数据,务必设置过期时间。
# 设置带过期时间的 key SET session:abc123 "user_data" EX 3600 # 1小时过期 SETEX cache:product:1 86400 "..." # 1天过期 # 给已有 key 补加过期时间 EXPIRE old_key 7200 EXPIREAT old_key 1735689600 # Unix 时间戳 # 批量查找没有 TTL 的 key(危险信号!) redis-cli --no-auth-warning -a pwd \ --scan --pattern '*' | \ xargs -L 100 redis-cli TTL | grep -1
设置内存上限,并选择合适的淘汰策略,让 Redis 自动清理「不常用」的数据。
# redis.conf 配置 maxmemory 2gb # 内存上限 maxmemory-policy allkeys-lru # 淘汰策略 # 常用淘汰策略说明: # noeviction - 满了就报错(默认,生产慎用) # allkeys-lru - 全局 LRU,推荐缓存场景 # volatile-lru - 只淘汰有过期时间的 key(LRU) # allkeys-lfu - 全局 LFU(Redis 4.0+) # volatile-ttl - 优先淘汰快过期的 key # allkeys-random - 随机淘汰(不推荐)
在客户端对 value 进行压缩(如 gzip/snappy/lz4),再存入 Redis,大 value 压缩率可达 60%~80%。
# Java 示例:存储时压缩 byte[] compressed = GzipUtils.compress(jsonString.getBytes()); redisTemplate.opsForValue().set("big_data:1", compressed); # Python 示例 import gzip, json data = json.dumps(large_dict).encode() r.set("big_data:1", gzip.compress(data, compresslevel=6)) # 注意:小 value 不值得压缩(压缩反而会更大) # 建议 value 原始大小 > 500 字节再考虑压缩
JSON 可读性好但体积大。改用 MessagePack、Protobuf 等二进制格式可节省 30%~60% 空间。
# 格式体积对比(同一份数据) # JSON: {"id":1001,"name":"张三","score":98.5} → 43 字节 # MessagePack: 二进制编码 → ~25 字节 # Protobuf: 二进制编码 → ~15 字节 # Python 使用 msgpack import msgpack packed = msgpack.packb({"id": 1001, "name": "张三"}) r.set("user:1001", packed) # 另一个方向:key 名也要精简! # ❌ SET user:profile:details:1001 ... # ✅ SET u:1001 ...(节省 key 本身的内存)
随着大量 key 的创建/删除,Redis 内存会出现碎片化。Redis 4.0+ 支持在线碎片整理(active defrag)。
# redis.conf 或运行时执行 activedefrag yes active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才启动 active-defrag-threshold-lower 10 # 碎片率 > 10% 开始整理 active-defrag-threshold-upper 100 # 碎片率 > 100% 全力整理 # 运行时动态开启(无需重启) CONFIG SET activedefrag yes # 检查当前碎片率 INFO memory # mem_fragmentation_ratio: 1.5 表示碎片率 50%
不要把所有数据都放 Redis。冷数据(历史记录、低频访问)放 MySQL/Elasticsearch,Redis 只存热数据。
# 冷热分离策略示意 # 热数据(近7天)→ Redis(高速访问) # 温数据(近30天)→ MongoDB(次级缓存) # 冷数据(30天以上)→ MySQL / 对象存储 # 定期扫描大 key(找出内存"黑洞") redis-cli --bigkeys -a password # 或使用 memory usage 命令精确统计 MEMORY USAGE big_key_name # 返回字节数
对特殊场景,用专门的数据结构可节省 95%+ 内存:
# 场景1:统计 UV(去重计数) # ❌ Set:每个用户 ID 存一条 → 百万级 Set 占 10MB+ # ✅ HyperLogLog:固定 12KB,误差 0.81% PFADD uv:2024-01-01 user1 user2 user3 PFCOUNT uv:2024-01-01 # 返回估算值 # 场景2:布隆过滤器(判断是否存在,不存误判) # ❌ Set 存 1 亿黑名单 → ~1.6GB # ✅ Bloom Filter:同等量级仅需 ~150MB # 需要 RedisBloom 模块(Redis Stack 内置) BF.ADD blacklist "bad_user_123" BF.EXISTS blacklist "bad_user_123" # 场景3:位图计数(签到、在线状态等) # 1 亿用户的签到状态:Bitmap 只需 12.5 MB SETBIT sign:2024-01 1001 1 # 用户 1001 签到 BITCOUNT sign:2024-01 # 统计签到人数
| # | 方法 | 节省效果 | 实施难度 | 适用场景 |
|---|---|---|---|---|
| 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%+ |
|
统计、去重、判存在 |
redis-cli --scanmaxmemory 和 allkeys-lru 策略redis-cli --bigkeys 找出大 key 并拆分activedefrag yes 自动碎片整理
随时运行 INFO memory 查看 used_memory_human(实际使用)、mem_fragmentation_ratio(碎片率);
运行 MEMORY DOCTOR 可获得 Redis 自动诊断建议;
使用 OBJECT ENCODING key_name 查看某个 key 的实际编码方式,确认是否命中紧凑编码。