模式之后,落地的三道坎
上篇把缓存读写模式讲透了,有同学说:模式我懂了,可真要把数据塞进 Redis,第一反应还是懵——键叫什么名字?内存给多少?一条数据多大算大?这三个问题看着琐碎,却是每个 Redis 使用者每天都要面对的工程细节,处理不好就是线上事故。
这一篇我们把「怎么塞」一次讲清:键设计、容量规划、大小 value 治理。
键设计:命名即文档
Redis 是键值数据库,键是你操作数据的唯一入口。键名起得好,排查问题、批量管理、监控分析全都不费力;起得差,三个月后你自己都不知道这个 key 是干嘛的。
社区惯例:冒号分层
键名的通行规范是 业务:对象:id,用冒号做层级分隔。看一组对比:
# 坏键名:看不出业务、掺了可变内容、超长
data_20260914_user_550e8400e29b41d4a716446655440000.json
# 好键名:三段结构,业务清晰
user:profile:10086
article:views:9527
order:list:10086:3好键名的好处不只是好看,它直接决定了三件事:
- 可批量操作:按前缀 SCAN 一把就能捞出某类数据(
article:views:*),清理、迁移、统计都靠它; - 可视化友好:RDM 等工具会按冒号自动折叠成树,一眼看清业务分布;
- 监控可归因:慢日志、大 key 报告里的键名带业务前缀,一眼定位到代码。
两条红线
第一,键里不要放可变内容。时间戳、随机数、毫秒级日期拼进键名,等于每次访问都在创建新键,旧键没人清理就变成内存黑洞——这通常是代码 bug,但只有键名规范清晰时才容易发现。第二,控制键名长度。键本身也占内存,而且元数据开销不小(下节细算),键名建议控制在 100 字节以内,用业务词汇,别用无意义缩写。
容量规划:别等 OOM 才想起来
Redis 最经典的翻车方式不是慢,是内存打满:maxmemory 到顶,写入开始报错,或者触发了没想到的淘汰策略,热数据被悄悄清掉。要避免这个局面,上线前先做估算:
容量估算 = 单条 value 平均大小 × 预计条数 × 冗余系数(1.5~2)
例:用户资料缓存
单条 JSON 约 1KB,目标缓存 1000 万用户
1KB × 1000万 × 2 = 20GB
→ 单机 32GB,maxmemory 建议配 24GB
→ 超过这个量级就该考虑分片集群公式里最容易被低估的是键的元数据开销。每个键在 Redis 内部除了 value 本身,还有 dictEntry、SDS 头、对象头等固定开销,粗算 60~90 字节。这意味着:存 100 万个 100 字节的小 value,光键元数据就要吃掉几十 MB,占比比数据本身还高。所以超小 value 优先合并存储(用 hash 把多个小字段归到一个键下),而不是一个数据一个键。
另一个经验值:内存使用率到 70% 就要警惕。Redis 做 RDB 持久化时要 fork 子进程,写时复制会让内存短时间内逼近翻倍;碎片率一高,实际占用还会再涨一截。70% 是从容处理的红线,不是极限。
maxmemory 配多少、八种淘汰策略怎么选,第 11 篇调优专题会展开。这里先记住:容量规划在上线前做,淘汰策略是兜底不是规划。
大 value 治理:单线程的阿喀琉斯之踵
Redis 是单线程处理命令的,这意味着任何一条慢命令都会阻塞所有请求。大 value 就是天然的慢命令制造者:读它要占带宽,删它要同步释放内存,迁移它能让集群卡顿。多大的 value 算大?业界参考阈值:
| 数据类型 | 大 key 参考阈值 | 主要风险 |
|---|---|---|
| String | value 大于 10KB | 网络带宽、阻塞 |
| Hash / List / Set / ZSet | 元素数大于 5000 或总大小大于 1MB | 操作阻塞、删除卡顿 |
先找到它们
# 扫描全库大 key(生产环境低峰期跑,控制采样节奏)
redis-cli --bigkeys -i 0.01
# 查看单个键的真实内存占用
MEMORY USAGE user:profile:10086治理四招
找到大 key 之后,按场景选招:
- 拆分:大 hash 按字段取模拆成多个子 hash,把单键操作压力摊开;
- 压缩:JSON 先 Gzip 再存,文本类数据能压掉 70% 以上体积,代价是一次序列化开销;
- 换结构:把整个对象 JSON 拆成 hash 的多个 field,读取时只取需要的字段,不再全量传输;
- 只存 id:缓存里只放 id 列表,详情按需回源查库——适合详情大、列表操作频繁的场景。
举个例子,用户画像这种 50KB 的大 JSON,最常用的解法是拆成 hash:
// 大 hash 拆桶:按 id 取模拆到 100 个子 hash
public void cacheUser(User user) {
long bucket = user.getId() % 100;
redisTemplate.opsForHash().put("user:hash:" + bucket,
String.valueOf(user.getId()), toJson(user));
}
public User getUser(Long id) {
Object v = redisTemplate.opsForHash().get(
"user:hash:" + (id % 100), String.valueOf(id));
return v == null ? null : toUser(v);
}拆完之后单键大小从 50MB 降到 500KB,读写不再阻塞,删除也能分批做。至于删除本身也有讲究:几百万元素的集合用 DEL 会卡顿,要用 UNLINK 异步释放,或者 HSCAN 分批删。
小结
- 键名:业务:对象:id 冒号分层,键里不放可变内容,长度控制在 100 字节内;
- 容量:单条大小 × 条数 × 冗余系数,上线前估算,内存 70% 是警戒线,小 value 记得合并存储省元数据;
- 大 value:String 大于 10KB、集合大于 5000 元素就该警惕,用 --bigkeys 找出来,拆分 / 压缩 / 换结构 / 只存 id 四招治理。
到这里,缓存的「选型」和「落地」都齐了。下一篇进入经典三问的第一问——缓存穿透:恶意请求拿着不存在的 id 疯狂打库,缓存形同虚设,四道防线怎么布?下篇见。
咖啡刚好到手,温度正好。
评论 (0)