











"慢了?加个缓存"——几乎是性能优化的第一反应。但缓存加完,新的麻烦才开始:穿透、击穿、雪崩、缓存和数据库对不上……这些坑大多是线上出过事才认识的。
这篇把缓存最该懂的几件事讲清楚:为什么快、怎么用、用在哪、三大坑怎么防,以及最难的——一致性。配套一个零依赖 demo,
node demo.js就能看到缓存的"一生"和每个防护的效果。
为什么快?因为数据在内存里。 内存读写是纳秒级,磁盘是毫秒级,差了几个数量级。把热点数据从数据库搬到内存,读取就快上千倍。最常用的缓存是 Redis——一个内存型的键值库。
缓存挡在应用和数据库中间:大部分读请求被缓存接住,只有少数落到数据库。所以缓存最适合读多写少的数据——用户信息、商品详情、配置、排行榜。
怎么用?最常见的是 Cache-Aside(旁路缓存)模式:
读: 请求 → 查缓存 → 命中? → 是 → 返回
↓ 否
查 DB → 写回缓存 → 返回
衡量缓存有没有用,就看一个指标:命中率。10 次请求 9 次走缓存,命中率 90%,数据库压力降到原来的 1/10。demo 场景一就是这个效果:连读同一条数据 10 次,回源次数从 10 降到 1。
命中率上不去,缓存就是白搭,甚至帮倒忙(多一次缓存查询还得回源)。所以缓存用得好不好,本质是命中率高不高——而下面三大坑,坑的就是命中率。
很多人以为缓存就是"把数据库的数据复制一份到 Redis"。其实缓存在业务里有两种角色,搞混了容易踩坑。
数据的"家底"在数据库,Redis 里放一份副本,专门给"读"提速。最典型的是商品详情页:
product:123 为 key 缓存进 Redis:key: product:123
value: {"id":123,"title":"xx 保温杯","images":[...],"specs":{...}}
TTL: 10 分钟
product:123 → 下次有人看时自动重建。这类缓存是数据库的副本,可丢、可重建,丢了只是慢一下、不影响数据安全;但也因此带来"和数据库一致"的问题(见第四节)。
注意:不是所有字段都适合塞进这份缓存。价格、库存这种要准、变得勤的,经常单独处理、甚至直接读库——缓存里的库存一旦不准,会超卖。
有些数据本身就该放内存、本就短命,压根没有"数据库副本"一说,Redis 直接当它们的主存储。最典型的是登录相关:
| 数据 | 为什么放 Redis |
|---|---|
| 登录态 / session | 每个请求都要校验"你是谁",超高频读 |
| 短信验证码 | 天生短命(5 分钟),带 TTL、验证完即删 |
| 登录失败次数 / 限流 | "5 分钟内失败 5 次就锁定",原子自增 + TTL 正合适 |
| 点赞数 / 阅读数 | 原子自增,异步落库 |
这类数据不存在"和数据库一致"的问题——它本身就住在 Redis 里。
一句话区分:角色一是"数据库的快副本"(商品、用户信息),角色二是"短命数据的家"(session、验证码、计数器)。后面讲的三大坑、一致性,主要针对角色一。
还有第三种更进阶的用法:用 Redis 的原子操作扛高并发扣减(比如秒杀库存)——那属于"高并发 / 秒杀架构"的话题,这里不展开。
这三个名字听着像,其实是三种不同的失效方式,共同点是:让请求绕过缓存,直接砸到数据库。一个个拆。
什么情况:请求一个数据库里也没有的 key——比如 id = -1、不存在的用户名。缓存永远不会命中(没数据可缓存),每次都打到数据库。常见于恶意攻击(狂刷不存在的 id)或爬虫乱爬。
为什么疼:缓存形同虚设,数据库直面全部流量。
怎么防:
取舍:缓存空值会占点内存,且如果那个 key 之后真被写入了,要记得让空值失效;布隆过滤器有极小误判率、且元素不好删除。
什么情况:某个热点 key(比如爆款商品)过期的那一瞬间,大量并发请求同时未命中,一起涌向数据库重建缓存。
注意它和雪崩的区别:击穿是一个热点 key 失效,雪崩是一大批 key 失效。
为什么疼:平时这个 key 替数据库扛着大流量,它一失效,这些流量瞬间全压到数据库上。
怎么防:
取舍:互斥锁实现简单,但重建那一下其他请求要等;逻辑过期不阻塞,但会短暂返回旧数据。
什么情况:两种——① 大量 key 设了相同的 TTL,到点集体过期,流量同时砸向数据库;② 缓存服务(Redis)整个宕机,所有请求瞬间穿到数据库。
为什么疼:数据库承受的是"全量"流量,直接被压垮,还可能引发连锁故障(数据库挂 → 拖垮上游服务)。
怎么防:
一句话:穿透、击穿是"点"的问题(单个 key),雪崩是"面"的问题(一大片),所以雪崩的防护更偏架构层——高可用、多级、限流降级。
🌰 生活里见过的:某明星突发热搜,几千万人瞬间涌进同一个话题页——这个超级热点的缓存一旦失效,海量请求同时砸向数据库(击穿),数据库扛不住就连环雪崩,于是你看到"微博又崩了"。应对靠的就是上面这套:热点 key 不过期 + 多级缓存 + 限流降级(崩的时候先降级——热搜暂缓更新、评论排队,保住核心别全挂)。
三大坑速查:
| 坑 | 触发 | 一句话 | 主要防护 |
|---|---|---|---|
| 穿透 | 查不存在的数据 | 缓存里永远没有,次次打 DB | 缓存空值、布隆过滤器 |
| 击穿 | 单个热点 key 过期瞬间 | 一个热点失效,并发全涌入 | 单飞互斥、逻辑过期、热点不过期 |
| 雪崩 | 大批 key 同时过期 / 缓存宕机 | 一片同时失效,DB 被压垮 | TTL 抖动、多级缓存、高可用、限流降级 |
缓存是数据库数据的一份"副本"。只要有两份数据,就有对不上的风险。更新数据时,数据库和缓存都要动,顺序和方式没选好,就会读到脏数据。
这里有两个要做的选择:更新缓存还是删除缓存?先动缓存还是先动数据库?
直觉上,数据变了,把缓存也改成新值不就行了?但更新缓存有几个问题:
所以主流做法是:更新数据库,然后删除缓存——让下次读请求未命中时,由 Cache-Aside 自然回填最新值。demo 场景五就是这个:更新后缓存被删,再读时重查数据库,拿到新值。
想让缓存和数据库时时刻刻强一致,代价极高——几乎要把缓存的性能优势抵消掉,实际工程里很少这么做。
通行的做法是接受最终一致:允许极短时间的不一致,靠"删缓存 + 下次回填" 加 TTL 兜底,让数据最终对上。对绝大多数业务(商品详情晚一秒更新没人在意),这完全够用。
但对一致性要求极高的数据——钱、库存——要么别缓存、直接读数据库,要么上更重的方案:分布式锁,或者订阅数据库 binlog(如阿里开源的 Canal)把变更同步到缓存。
🌰 生活里见过的:抢火车票"明明显示有票,付款却说没票"——你看到的余票是缓存里的数(为扛住海量查询,它不实时精确,可能已被抢光、只是还没更新);而付款时扣的是真实库存(以数据库为准)。系统宁可让你"看到有票却付款失败",也绝不超卖。这不是 bug,正是"缓存做近似的快查询、数据库做最终裁决"这个取舍的结果。
缓存不是免费的。它给系统多塞了一份"要维护一致性的副本",复杂度实打实地上升。所以加之前先想清楚——
不适合缓存的情况:
加缓存前先问三句:这数据读多写少吗?能容忍短暂不一致吗?命中率会高吗?——三个都是 yes,再加。
最后提醒一句:别把缓存当银弹。很多慢查询的正解是加索引、优化 SQL,而不是盖一层缓存把问题藏起来——藏起来的问题,迟早以更难查的方式冒出来。
把全文的招式对到真实业务,一张表带走:
| 业务场景 | 怎么处理 | 对应招式 |
|---|---|---|
| 商品详情页 | Cache-Aside 缓存拼好的商品 JSON | 旁路缓存(角色一) |
| 爆款商品大促 | 热点 key 过期 → 单飞重建 | 防击穿 |
| 大促大量商品缓存同时到期 | TTL 加随机抖动打散 | 防雪崩 |
| 改价格 / 改资料 | 更新库 + 删缓存 | 一致性 |
| 恶意刷不存在的商品 id | 缓存空值 / 布隆过滤器 | 防穿透 |
| 排行榜 / 热搜 | Redis ZSet(自动按分数排序),定时刷新 | 短命数据主存储(角色二) |
| 点赞数 / 阅读数 | 原子自增,异步落库 | 短命数据主存储(角色二) |
| 库存扣减 / 余额 | 别简单缓存——读库 / 分布式锁 / 秒杀另算 | 什么时候别加缓存 |
配套 demo:backend-notes/01-data/caching-demo ——
node demo.js一跑:场景零打印缓存的一生(创建 / 命中 / 删除),后面五个场景看每个防护把回源次数压下去的效果。本文属《研发都要懂的事》· 数据存储专题。上一篇:存储与基建选型 —— 传统业务 vs AI 大模型时代。完整代码与系列在 GitHub · backend-notes。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。