Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • [Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字------内存构成与容量规划](#Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划)
    • [一、现象:数据 1G,used_memory 2G](#一、现象:数据 1G,used_memory 2G)
    • [二、INFO memory 的 8 个关键字段(收藏点 1)](#二、INFO memory 的 8 个关键字段(收藏点 1))
    • [三、编码转换:同一份数据,内存差 5 倍](#三、编码转换:同一份数据,内存差 5 倍)
    • [四、Go 实操:编码转换的内存测量(收藏点 2)](#四、Go 实操:编码转换的内存测量(收藏点 2))
    • [五、容量规划公式(收藏点 3)](#五、容量规划公式(收藏点 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 倍------这就是"数据明明一样多,内存却对不上"的常见原因。

bash 复制代码
OBJECT ENCODING user:1001    # 看当前编码
CONFIG GET hash-max*         # 看阈值

四、Go 实操:编码转换的内存测量(收藏点 2)

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"

	"redis-0to1/redisx"
)

func main() {
	rdb, err := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 10})
	if err != nil {
		panic(err)
	}
	ctx := context.Background()

	// 对照实验:128 个 field(listpack)vs 200 个 field(hashtable),同 field 大小
	measure := func(n int) {
		key := fmt.Sprintf("memtest:%d", n)
		pipe := rdb.Pipeline()
		for i := 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,约 5KB
	measure(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 分片

六、排查步骤:内存超标时按顺序看

  1. INFO memory 拍全景:dataset vs overhead vs rss 三分法定位大类;
  2. MEMORY USAGE 抽查可疑大 key(第 22 篇);
  3. OBJECT ENCODING 抽查高频 key 是否全在 hashtable(阈值被业务数据形态踩穿);
  4. 对照公式重算容量规划,决定扩内存还是拆 Cluster。

七、总结

  1. used_memory ≠ 数据量:结构开销、编码、缓冲、碎片四个口袋;
  2. 编码转换点(listpack→hashtable)是内存的隐形台阶,阈值可配可调可观测;
  3. 容量规划五步法:数据 × 结构系数 + key 开销 + 缓冲 + 碎片 + maxmemory 打 7 折;
  4. 小 Hash/小 ZSet 保持 listpack 是免费的内存优化。

下一篇预告:内存省了一半,网络传输还能再省------下一篇《JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试》,用 Benchmark 数据说话,选对你的 Codec。

相关推荐
我不会起名字32236 分钟前
Redis 从 0 到 1(二十五):连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats
redis·连接池·go-redis·排查·poolstats
我不会起名字32239 分钟前
Redis 从 0 到 1(二十三):一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现
redis·缓存·go·qps·hotkey
IT规划师2 小时前
Redis 数据类型全解 - Bitmap 生产实战:把位视图讲准、用对、守住红线(六)
redis·.net
Fatbobman(东坡肘子)14 小时前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
温暖小土15 小时前
在 CentOS 上安装 Redis Stack
linux·redis·centos
Mikko718 小时前
Redis 连接数满了服务端有错误信息吗?连接数不增加却命令超时?6.2 实测连接池、超时与大 key 怎么判定
java·spring boot·redis·后端
无言听风雨丶18 小时前
Redis 运维实战与排错
运维·数据库·redis
字节探索1 天前
Redis 分布式锁:从 SETNX 误删到看门狗自动续期的原理与实践
redis·分布式