1. 缓存入门:为什么需要缓存、缓存的工作原理、缓存带来的挑战
各位同学,咱们今天聊聊缓存。说实话,我做了十几年后端,见过太多系统因为没用好缓存而「翻车」的例子。有一次线上告警,数据库连接池被打满,CPU 飙到 99%,我一看——好家伙,同一个查询每秒跑了上万次。加了个缓存,响应时间从 800ms 降到了 5ms。你说,这玩意儿重不重要?
1.1 为什么需要缓存
先问个问题:你的 API 慢在哪?绝大多数情况下,瓶颈都在 I/O——读数据库、调远程服务、访问磁盘文件。这些操作动辄几十毫秒甚至几百毫秒,而内存访问只要几微秒。缓存说白了,就是把「慢操作」的结果存到「快介质」里,下次直接拿。
我归纳了三个核心场景:
- 减轻数据库压力:高频查询不走 DB,缓存扛住 90% 的流量
- 降低响应延迟:内存读取 vs 磁盘/网络 I/O,差两个数量级
- 提升系统吞吐:同样的硬件,缓存命中率高时能多扛几倍并发
一个真实数据:我在某电商项目里做过压测,商品详情页不加缓存时 QPS 只有 800,加了一层 Redis 本地缓存后,QPS 直接冲到 12000。不是算法优化,就是缓存。
1.2 缓存的工作原理
缓存的核心逻辑其实就四个字:先查缓存,再查源。画个图你就明白了。
流程很简单:
- 请求来了,先查缓存
- 缓存有数据(命中),直接返回——完事
- 缓存没数据(未命中),查数据库,把结果写回缓存,再返回
代码实现也很直白。我用伪代码写一下核心逻辑:
function getData(key) {
// 1. 先查缓存
data = cache.get(key)
if (data != null) {
return data // 命中,直接返回
}
// 2. 未命中,查数据库
data = db.query("SELECT * FROM table WHERE id = ?", key)
// 3. 写回缓存,设置过期时间
cache.set(key, data, ttl=3600)
return data
}
我的习惯:生产环境里,我一般把缓存 TTL 设成业务能接受的最大延迟。比如商品详情页,允许 5 分钟延迟更新,TTL 就设 300 秒。别设太长,否则数据不一致会让你头疼。
1.3 缓存带来的挑战
缓存不是银弹。用不好,反而会引入三个大坑:一致性、雪崩、穿透。我一个一个说。
1.3.1 缓存一致性
说白了就是:数据库改了,缓存还是旧数据。用户看到的是「过期」内容。
我在项目中遇到过最典型的场景:用户修改了个人信息,数据库已经更新了,但缓存没清掉。结果用户刷新页面,看到的还是老头像。气得用户直接投诉。
常见的处理方式有三种:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 先更新 DB,再删除缓存 | 写 DB → 删缓存 | 实现简单,最终一致 | 删除可能失败,需重试 |
| 先删缓存,再更新 DB | 删缓存 → 写 DB | 避免读到旧数据 | 写 DB 期间缓存为空,压力大 |
| 延迟双删 | 删缓存 → 写 DB → 延迟再删 | 一致性较高 | 实现复杂,有延迟窗口 |
我曾经踩过的坑:用了「先删缓存,再更新 DB」的策略,结果在并发场景下,另一个线程在删缓存后、写 DB 前读到了旧数据并写回缓存。最后不得不改成「延迟双删 + 消息队列兜底」才解决。嗯,一致性这事,没有完美的方案,只有适合业务的方案。
1.3.2 缓存雪崩
想象一下:你的缓存里存了 100 万条数据,全部在同一时刻过期。下一秒,所有请求都穿透到数据库——数据库瞬间被打爆。这就是缓存雪崩。
为什么会这样?很多时候是因为 TTL 设成了固定值。比如所有商品缓存都设 3600 秒,那每小时整点就是一场灾难。
怎么防?我总结了三个办法:
- TTL 加随机偏移:基础值 3600 秒,再随机加 0~600 秒,避免集体过期
- 多级缓存:本地缓存 + Redis + 数据库,层层兜底
- 限流降级:缓存失效后,对 DB 查询做限流,保护后端
// 伪代码:TTL 加随机偏移
int baseTtl = 3600;
int randomOffset = new Random().nextInt(600); // 0~600 秒随机
cache.set(key, data, baseTtl + randomOffset);
1.3.3 缓存穿透
缓存穿透和雪崩不一样。雪崩是缓存有数据但集体过期,穿透是缓存和数据库都没有这个数据。比如恶意攻击者拿一个不存在的用户 ID 疯狂请求,每次都不命中缓存,直接打到数据库。
我见过最夸张的一次:某系统被爬虫用自增 ID 遍历,大量不存在的 ID 导致数据库 CPU 100%。加缓存没用,因为根本不存在的数据缓存里也没有。
解决方案:
- 缓存空值:查询结果为空时,也缓存一个「空标记」,TTL 设短一点(比如 60 秒)
- 布隆过滤器:请求来之前先判断 key 是否存在,不存在直接拒绝
- 参数校验:不合法的 ID 直接返回 400,不查缓存也不查 DB
我的建议:布隆过滤器虽然好用,但要注意它有误判率(会把不存在判成存在)。我一般把误判率设成 1%,然后配合缓存空值做兜底。两层防护,基本能挡住 99% 的穿透攻击。
1.4 本章小结
缓存的核心就三件事:为什么用、怎么用、有什么坑。
- 为什么用?——快,省资源,扛并发
- 怎么用?——先查缓存,未命中再查源,写回缓存
- 有什么坑?——一致性要权衡,雪崩要防集体过期,穿透要防空值攻击
记住一句话:缓存是加速器,不是救世主。用得好,API 响应快十倍;用得不好,半夜爬起来修数据。后面的章节,我会带你深入每种策略的细节和实战代码。咱们一步步来。