我不会起名字322· 后端 / 算法 / 数据库
📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL
文章目录
- Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划
- 一、现象:数据 1G,used_memory 2G
- 二、INFO memory 的 8 个关键字段(收藏点 1)
- 三、编码转换:同一份数据,内存差 5 倍
- 四、Go 实操:编码转换的内存测量(收藏点 2)
- 五、容量规划公式(收藏点 3)
- 六、排查步骤:内存超标时按顺序看
- 七、总结
Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划
上一篇《Redis 从 0 到 1(二十五):连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats》我们把连接池调成了理性状态,链接:https://blog.csdn.net/2501_92769340/article/details/167399911 。
本篇往内存里看:Redis 的内存到底花在哪了,怎么规划才不被半夜的 OOM 叫醒。
一、现象:数据 1G,used_memory 2G
运维同学扩容前做容量核对:INFO memory显示 used_memory 2.1GB,而业务自己估算的数据量只有 1GB。“另一半被谁吃了?”
答案通常藏在四个口袋里:数据结构开销、编码差异、缓冲区、碎片。本篇把这四个口袋逐个拆开,给每个数字一个名字。
二、INFO memory 的 8 个关键字段(收藏点 1)
| 字段 | 含义 | 关注点 |
|---|---|---|
used_memory | Redis 自己记账的数据+结构总字节 | 容量规划基准;admin 视角 |
used_memory_rss | 操作系统视角实际占用(含碎片、页缓存) | 与 used_memory 的比值见下行 |
mem_fragmentation_ratio | RSS / used_memory | 1.0~1.1 健康;>1.5 警惕碎片(第 30 篇);<1.0 说明 swap 或共享内存 |
used_memory_dataset | 纯数据部分 | 业务真实数据量 |
used_memory_overhead | 结构开销(dictEntry、redisObject、过期字典) | overhead/dataset 比例反映 key 数量级开销 |
used_memory_lua | Lua 引擎占用(本系列不用 Lua,此项很小) | 异常增长要查 |
mem_clients_normal / mem_clients_slaves | 客户端输出缓冲 | slave 缓冲大 = 复制卡了(第 8 篇来源 3) |
used_memory_startup | 启动基础开销 | 常数,无需关注 |
经验诊断:
- used_memory 涨但 used_memory_dataset 不涨→ 结构开销在涨:key 太多或大量空结构;
- overhead 占一半以上→ key 设计有问题(短 key 千万级时 overhead 能超过数据本身,第 4 篇的 key 长度规范就是为它定的);
- rss >> used_memory→ 碎片(下一篇)。
三、编码转换:同一份数据,内存差 5 倍
Redis 每种结构都有"紧凑编码"和"普通编码"两档,按元素数量/大小自动转换:
| 结构 | 紧凑编码 | 转换阈值(redis.conf 可调) | 普通编码 |
|---|---|---|---|
| Hash | listpack | hash-max-listpack-entries 128、hash-max-listpack-value 64 | hashtable |
| ZSet | listpack | zset-max-listpack-entries 128 | skiplist |
| List | listpack | list-max-listpack-size 128 | quicklist |
| Set | intset | set-max-intset-entries 512 | hashtable |
含义:一个 100 个 field 的小 Hash 走 listpack,内存接近纯数据;第 129 个 field 触发转换整体搬迁成 hashtable,内存瞬间翻几倍。同一份数据,128 个 field 和 129 个 field 的内存差可能是 3 倍——这就是"数据明明一样多,内存却对不上"的常见原因。
OBJECT ENCODING user:1001# 看当前编码CONFIG GET hash-max*# 看阈值四、Go 实操:编码转换的内存测量(收藏点 2)
packagemainimport("context""fmt""time""redis-0to1/redisx")funcmain(){rdb,err:=redisx.NewClient(redisx.Config{Addr:"127.0.0.1:6379",Password:"redis123",PoolSize:10})iferr!=nil{panic(err)}ctx:=context.Background()// 对照实验:128 个 field(listpack)vs 200 个 field(hashtable),同 field 大小measure:=func(nint){key:=fmt.Sprintf("memtest:%d",n)pipe:=rdb.Pipeline()fori:=0;i<n;i++{pipe.HSet(ctx,key,fmt.Sprintf("f%03d",i),"value-of-same-length-32bytes")}pipe.Exec(ctx)enc,_:=rdb.ObjectEncoding(ctx,key).Result()usage,_:=rdb.MemoryUsage(ctx,key,0).Result()fmt.Printf("fields=%d 编码=%s 内存=%d 字节\n",n,enc,usage)rdb.Del(ctx,key)}measure(128)// 典型结果:编码=listpack,约 5KBmeasure(200)// 典型结果:编码=hashtable,约 15KB+(3 倍差)}(数字随版本略有出入,量级不变:转换点前后内存差数倍。)
五、容量规划公式(收藏点 3)
实例容量规划: 1. 数据量 = 单条平均字节 × 条数 × 结构系数(listpack 小对象约 1.2,hashtable 约 2~4) 2. 加 key 开销:key 数 × (dictEntry+redisObject ≈ 100 字节) 3. 加缓冲区余量:AOF 重写缓冲区 + 复制缓冲 ≈ 数据量的 10%~20% 4. 加碎片余量:RSS = used_memory × 1.1~1.3(第 30 篇) 5. maxmemory 设为物理内存的 70%(给 fork 留出 COW 空间,第 7 篇 RDB 需要) 例:1 亿 key、单 value 500B、平均 3 个 field 的 Hash → 数据 50GB × 结构系数 2 + key 开销 10GB + 缓冲 12GB ≈ 122GB → 实例物理内存 ≥ 122GB / 0.7 ≈ 175GB → 一台装不下就上 Cluster 分片六、排查步骤:内存超标时按顺序看
INFO memory拍全景:dataset vs overhead vs rss 三分法定位大类;MEMORY USAGE抽查可疑大 key(第 22 篇);OBJECT ENCODING抽查高频 key 是否全在 hashtable(阈值被业务数据形态踩穿);- 对照公式重算容量规划,决定扩内存还是拆 Cluster。
七、总结
- used_memory ≠ 数据量:结构开销、编码、缓冲、碎片四个口袋;
- 编码转换点(listpack→hashtable)是内存的隐形台阶,阈值可配可调可观测;
- 容量规划五步法:数据 × 结构系数 + key 开销 + 缓冲 + 碎片 + maxmemory 打 7 折;
- 小 Hash/小 ZSet 保持 listpack 是免费的内存优化。
下一篇预告:内存省了一半,网络传输还能再省——下一篇《JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试》,用 Benchmark 数据说话,选对你的 Codec。