2399 字
约 7 分钟
14
S12-A18 · 缓存值的大小:一个大 value 能拖垮整条链路

一个商品详情缓存把"完整详情 + 全部评价摘要 + 推荐列表"塞进一个 value,平均 400KB。上线后 Redis 网络出口先打满,然后是应用的反序列化 CPU 打满——而命中率一直很好,没人觉得有问题。

缓存值的大小是一个被忽略的成本维度:它决定了网络带宽、序列化开销、以及"取一次要多久"。这个"与命中率无关"是关键:命中率是"有没有拿到",不是"拿得划不划算"。所以一个命中率 99% 的缓存,仍可能因为"每次拿 400KB 只用 2KB"而成为瓶颈。
而这些成本随"值的大小 × 请求频率"增长,与"命中率"无关。


1. 四个成本项

成本 随什么增长
网络传输 值大小 × 请求数
序列化/反序列化 值大小 × 请求数(CPU)
内存占用 值大小 × 条目数
淘汰成本 值越大,一次淘汰释放越慢

第一项在"高频访问大 value"时最先爆:单次 400KB × 2000 QPS = 800MB/s,这个量级足以打满万兆网卡。

第三项在"条目多"时爆:10 万个 400KB 的 value = 40GB第一项(网络)还有个"放大"因素:取缓存与回源都要走网络,而大 value 让"缓存"这条路径的网络成本接近"回源"——于是缓存的价值被抵消。
,而多数团队按"条目数"规划容量,忽略了大小。

2. 值该多大:一个参考尺度

量级 适用
< 1KB 适合高频、大量条目
1–10KB 常见配置与详情
10–100KB 需评估带宽
> 100KB 通常该拆

这个尺度不是硬规则:图片类 value 常是几百 KB 且必须整块用,它不属于"该拆"的范畴。所以判据是"用多少"而不是"多大"。
第四项的建议来自"一次网络往返的价值":如果单次取 400KB 只为用其中 2KB,那 99% 的带宽是浪费的——而这类"取全量用一部分"的访问模式极常见。

// 一次请求的成本拆解(400KB value,2000 QPS)
网络:400KB × 2000 = 800MB/s(可能打满网卡)
反序列化:400KB → 对象 ≈ 1.2MB 临时内存,CPU 约 1–3ms
// 如果只用到其中 2KB,成本放大了 200 倍

3. 大 value 的四种拆法

拆法 适用
按"访问模式"拆 详情 / 评价 / 推荐分开
按"变化频率"拆 详情稳定、评价常变
存 ID 列表 + 二次取 列表类数据
存压缩后的值 文本类、可压缩

第一项最有效:把"总是一起用"的部分放一起,把"有时用"的拆出去。而多数大 value 的问题是"把不相关的东西放在一起"。

第二项减少失效成本:评价变了不该让详情失效,而合并存储会导致整块重算。第二项的额外收益在"降级":评价服务挂了,详情仍可读。而合并存储时,只要其中一部分不可用,整块就不可用——这也是"耦合"的一种形态。

// 拆分示例:商品详情页
key: item:{id}:base       ≈ 2KB   详情,TTL 1h
key: item:{id}:reviews    ≈ 8KB   评价摘要,TTL 5m
key: item:{id}:recs       ≈ 3KB   ID 列表,TTL 10m
// 收益:详情命中率最高、评价失效不影响详情、推荐可单独降级

4. 压缩:什么时候值得

数据类型 压缩比
JSON(含重复键) 3–8 倍
文本 2–4 倍
已压缩二进制 接近 1
短字符串 可能变大

第二项与第三项的边界要注意:对已压缩的数据再压缩,通常只增加 CPU。

第二项与第三项的差别值得说清:JSON 的压缩比高是因为"键重复出现",而二进制格式已经"去掉重复",所以再压缩收益小。判断方法:先看原始格式的"冗余程度"。
第四项是"小 value 压缩"的陷阱:压缩头本身有几个字节,1KB 以下可能得不偿失。

// 压缩的取舍:先量压缩比,再看 CPU
byte[] raw = serialize(value);            // 40KB
byte[] gz = gzip(raw);                    // 6KB(压缩比 6.7)
// 收益:网络省 34KB;成本:gzip 约 0.3ms、gunzip 约 0.2ms
// 判据:网络省下的时间 > CPU 花掉的时间(在高 QPS 下通常成立)

还有一个"批量取"的形态会放大问题:MGET 一次取 100 个 key,每个 400KB 就是 40MB 一次往返。这不仅慢,还会让 Redis 的单线程被"打包响应"占住。

// 批量取的体积估算(必须提前算)
MGET 100 key × 400KB = 40MB / 次
若 QPS 10 → 400MB/s,且 Redis 单线程要拼 40MB 响应
// 缓解:限制 batch 数量、按大小分片、或改用 pipeline + 流式消费

5. 序列化格式的影响

格式 大小 速度
JSON 大 慢
Java 序列化 中 中(且不跨语言)
Protobuf/MessagePack 小 快
紧凑二进制 最小 最快

第一项在调试友好性上最好,但它的体积与解析成本都是最高的——而多数团队用它只是为了"人能读"。

第三项在"大 value"上收益明显:字段名只出现一次(或编号代替),体积常能减半。第三项(Protobuf)的代价是"不可读":排障时不能直接看 value,需要工具解码。所以"人能读"这件事在缓存里往往不重要——缓存不是给人看的。

// 同一对象的体积对比(约 20 个字段)
JSON: 2.4KB   Protobuf: 0.9KB   // 体积差 2.6 倍
// 若 QPS 2000:省下 3GB/小时的网络流量

监控上要能"按大小分档"看缓存,否则问题不可见:

// 三个必须有的观测量
1 value 大小分布(P50/P99/max)      → 发现"少数大 value 占大头"
2 命中 vs 带宽消耗                    → 判断"命中是否值得"
3 序列化耗时(单次与总量)            → 判断 CPU 成本
// 只监控"命中率"时,这三个都看不到

6. 排错:五种"缓存拖慢了系统"

现象 成因 处理方向
Redis 带宽打满 大 value + 高 QPS 拆分或压缩
应用 CPU 高但 QPS 不高 反序列化开销 换格式或缩小值
内存增长快 条目 × 大小 加淘汰或缩小
取缓存比查库还慢 值太大 拆小或改直查
偶发超时 大 value 传输抖动 加超时与降级

第四项还有一个"冷启动"版本:大 value 在缓存刚预热时集中写入,会同时打满网络与 CPU,表现为"服务刚起来时特别慢"。
第四项是最反直觉的:缓存本应更快,但大 value 的传输 + 反序列化可能超过一次简单查询。判据是"实测两条路径的耗时",而不是"缓存一定更快"。

最后一个"演进"视角:缓存值的大小会随时间悄悄增长。今天 20KB 的详情,加了推荐、加了标签、加了 A/B 字段,半年后变成 200KB——而"加字段"这个动作本身没人会想到"要评估缓存成本"。

// 防渐变的两个动作
1 上线前记录 value 大小基线(写进接口文档)
2 大小增长超过 2 倍时触发评审(不是告警,是评审)
// 关键:它属于"设计漂移",而不是"故障",所以不能靠告警发现

7. 边界:什么情况大 value 是合理的

场景 为什么 做法
低频访问(分钟级) 带宽压力小 可接受
一次取用全部内容 无浪费 可接受
用于导出/下载 本来就是大块 可接受
已被压缩的媒体 压缩无收益 直存对象存储

判断口径一句话:这个 value 被"整块用"还是"只用一小部分"。最后一条与"容量规划"有关的边界:按"条目数"规划容量是常见错误,因为条目大小可能差两个数量级。正确做法是"按平均大小 × 条目数 + 最大值 × 安全系数"估算。
整块用,大小不是问题;只用一小部分,就说明"存的东西"与"取的目的"不匹配。


8. 自测

  1. 缓存值大小的四个成本项分别随什么增长?为什么"命中率很好"也可能有问题?
  2. 为什么"400KB × 2000 QPS"最先打满的是网络而不是 CPU?
  3. 四种拆法里,哪两种最有效?"按变化频率拆"解决了什么额外问题?
  4. 什么情况下"压缩"反而得不偿失?
  5. 为什么"取缓存可能比查库还慢"?判据是什么?
  6. 判断"大 value 是否合理"的那个问题是什么?
S12-A18 · 缓存值的大小:一个大 value 能拖垮整条链路
https://www.clxhxhhr.top/posts/6526/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。