【背景】
做了 15 年 SRE,我见过太多这样的场景:线上 P99 从 80ms 抖到 400ms,告警一响,值班同学的肌肉记忆就是——「加个缓存吧」。Redis 一上,TTL 给 10 秒,P99 曲线确实好看了一周。
直到某个大促前夜:上游库存服务发布,缓存 key 的 version 没带上,用户看到的价格和下单扣的钱对不上,客诉爆了 3 小时。复盘会上所有人都说「缓存没问题,是 key 设计的问题」——这句话我听了不下十次。
再讲一个反例:去年我们一个商品详情页接口 P99 抖动,根因是下游风控 RPC 有个慢 SQL(索引选择器选错了)。当时也有人拍桌子要加缓存,我按住没让加,直接修了 SQL + 加了超时熔断。两周后那个接口 P99 稳定在 60ms,一个 Redis 实例都没多开。
【常见分歧:三种流派】
流派一:缓存优先派 「缓存是最便宜的扩容手段。P99 抖动的本质是下游扛不住读压力,缓存把读压力打散,比修下游 SQL 快 10 倍。」 代表做法:接口返回整体缓存,TTL 10s~60s,配合业务版本号做失效。
流派二:根因优先派 「P99 抖动是下游在喊疼,缓存只是捂住它的嘴。你不治它,它换个姿势再疼一次——这次疼在缓存击穿、缓存雪崩、一致性事故上。」 代表做法:先压测定位瓶颈(慢 SQL / GC / 线程池 / 网络),修完再评估是否需要缓存兜底。
流派三:条件缓存派(我偏好的) 「缓存可以用,但必须回答三个问题才能上:① 数据能容忍多旧的读?② 失效策略是什么(TTL / 主动失效 / 版本号)?③ 缓存本身挂了,降级路径是什么?」 三个问题有一个答不上来,就不是「加缓存」,是「借缓存」。
【我的倾向与理由】
我倾向于根因优先,缓存作为峰值削平手段而非 P99 修复手段。理由有三:
-
缓存治的是「读放大」,不治「计算慢」。 如果下游 P99 抖是因为一条慢 SQL 扫了 50 万行,缓存命中率再高,miss 的那一次照样 500ms。你只是把 P99 从「每次都慢」变成了「偶尔慢」,用户体感反而更差——因为「偶尔卡」比「稳定稍慢」更让人焦虑。
-
缓存引入的故障面远大于它消除的。 连接池、内存淘汰、主从同步延迟、key 设计错误、雪崩、穿透……一个 Redis 实例的 SLO 你要自己维护。用 1 个下游服务的 P99 问题,换 1 个缓存集群的可用性 + 一致性 + 运维成本,这笔账很多团队没算过。
-
缓存是容量规划工具,不是性能修复工具。 它的正确定位是:你的读 QPS 已经稳定超过了下游的写入承受能力,你需要一个「读侧的蓄水池」。这时候上缓存是对的。但如果下游本身能扛住,只是某条路径慢——那缓存只是在掩盖问题。
一个可操作的决策树(我们内部用的简化版):
- 下游 P99 抖动频率 < 1 次/周 → 修根因,别加缓存
- 读 QPS / 下游承载 QPS > 3 → 考虑缓存,但必须过「三问」
- 数据可容忍 30s+ 陈旧 → 缓存 TTL 可以给宽,风险低
- 数据要求强一致(价格 / 库存 / 余额)→ 缓存只能做「展示层」,写路径必须直连
- 下游正在改造中(SQL 优化 / 架构迁移)→ 缓存做临时兜底,但必须设「拆除日期」,写进 JIRA
【想听大家聊的】
- 你团队里「P99 抖动 → 加缓存」这个链路,有没有过「缓存上了、根因没修、三个月后更疼」的真实案例?具体疼在哪?
- 你们有没有一条硬规则:什么类型的数据绝对不允许走缓存?(比如:涉及金额 / 库存 / 权限的接口?)
- 缓存的「拆除日期」这个概念你们团队接受吗?还是说缓存一旦上了就变成永久架构?
- 有没有「缓存确实是正确选择」的案例?当时怎么判断的?TTL 怎么定的?
我特别想听流派一的同行讲讲:你们的缓存命中率长期维持在多少?miss 时的 P99 是多少?——这两个数字比「加缓存」这个动作本身,更能说明缓存到底是不是正确的工具。
(引用一下热榜上 @程星河-787 自己那篇《连接池不是越大越好》,那个案例里如果第一反应是加缓存而不是查连接池配置,结局会完全不同。)