1. 这个坑到底是什么:一次线上内存告警揪出的“幕后黑手”
先讲个前几天刚发生的真实案例。凌晨两点多,业务群里的告警机器人突然开始刷屏,某台 Redis 实例的内存使用率直接冲过了 90%。我第一反应是缓存失效导致大量请求穿透,或者有人写了死循环在往 Redis 里灌数据。登录服务器看了一眼 INFO memory,used_memory 大约 4.2GB,再数了数 key 数量,答案瞬间就浮出水面了——上百个用户 Session 信息,每个 Session 被拆成了平均 7~8 个独立的字符串键来存,加起来一共 600 多万个 key,全堆在同一个实例里。
这就是标题里说的“正在榨干你内存的 Key 设计模式”——把本应该属于同一条业务数据的多个字段,平铺成大量独立的 String 类型键。很多刚接触 Redis 的同学会觉得这没什么问题,每个用户一个 key 存用户 ID,再一个 key 存昵称,再一个 key 存手机号,看起来逻辑清晰,甚至还会觉得“拆得越细越灵活”。但实际上,Redis 里每一条 String 键都不只是存一个 value 那么简单,它背后还有一套固定的元数据开销,这个开销在 key 数量达到百万、千万级别时,会变成一笔非常恐怖的内存账单。
下面这张对比表基本能说明问题:
| 存储方式 | 用户数量 | 键数量 | 单条键平均开销(含元数据) | 总内存占用 |
|---|---|---|---|---|
| 每个字段一个 String | 100万 | 约 600万 | 约 120~200 字节 | 约 1~1.2 GB |
| 每个用户一个 Hash | 100万 | 100万 | 约 50~100 字节 | 约 300~600 MB |
同样是 100 万用户的数据,光是键的设计方式不同,内存差距就可能在一倍以上。数据量越大,这个差距越明显。接下来我会从 Redis 的底层存储机制讲起,把为什么会出现这种差距、哈希键为什么更省内存、以及什么场景下可以放心用哈希、什么场景容易反噬,一次性讲透。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一条 String 键的“固定房租”:Redis 底层如何记账
2.1 一条 String 键,到底占了多少空间
要理解为什么键多了会吃内存,得先看清楚 Redis 在内存里到底为一条键值放了多少东西。Redis 在存储一个普通的字符串键值时,并不是简单地存一个“名字 + 内容”,而是至少包含以下四部分:
- 键对象本身(robj):Redis 中的每个对象都有一个“对象头”,内部包含类型、编码方式、最近访问时间、引用计数等信息。老版本里这个头部占 16 字节左右,4.0 之后有些字段做了调整,但依然存在。
- 键名的 SDS(简单动态字符串):Redis 自己实现了一个字符串结构,头部除了存储字符数组外,还要存长度、空闲空间等信息。键名越长,这个结构越大。
- 值对象的 robj 和 SDS:和键名一样,值本身也有同样的对象头和字符串结构开销。
- dictEntry(字典条目):Redis 是一个哈希表结构,每一条键值对都会生成一个 dictEntry 条目,这个条目里存着指向 key、value 和下一个冲突元素的指针,一般要占 24~48 字节。
把这些加起来,你就明白为什么网上经常说“一条 Redis 字符串键,空值也要占至少 50 字节”。如果你设置的键名本身就有 20~30 个字符,比如 user:12345:profile:name,那这个键名的 SDS 又要吃掉 30 多字节。一条看似轻量的 String,实际开销可能在 150 字节上下。
我见过最有意思的案例,是有人把一个 Java 对象序列化成 JSON 后直接塞进 Redis,json 里光字段名就有几十个,再加上外层 key 名长,一条数据轻松干掉几百字节内存。这时候如果你再加个过期时间,Redis 为了存 TTL 还要在 dictEntry 后追加一个 8 字节的绝对时间戳——也就是每一条带过期时间的键,还得额外付一笔“到期管理费”。
2.2 哈希键为什么能省钱:小账合并成大账
哈希键(Hash)在 Redis 里的存储方式,和普通字符串键有一个本质区别。Redis 会先看这个哈希有多少字段、每个字段的值有多长,然后自动选择内部编码:
- listpack(以前版本叫 ziplist):当字段数量很少且每个字段的值都很短时,Redis 会把整个哈希压缩成一个连续的内存块,所有字段和值像
