2. 缓存基础与常见模式:穿透、击穿、雪崩与读写策略
缓存是后端系统中提升读取性能最直接的手段,但若使用不当,反而会成为系统稳定性的隐患。本章从两类问题(缓存失效引发的故障)和三类模式(数据同步策略)入手,建立缓存中间件调优的底层认知。
2.1 三大缓存失效问题
缓存穿透、缓存击穿、缓存雪崩是面试与生产故障中最常出现的三个概念,其本质都是“请求绕过缓存直接打到数据库”,但触发场景与影响范围截然不同。
| 问题 | 触发条件 | 影响范围 | 典型后果 |
|---|---|---|---|
| 缓存穿透 | 查询一个不存在的数据(key 既不在缓存也不在数据库) | 单 key 持续攻击 | 数据库连接耗尽,慢查询堆积 |
| 缓存击穿 | 一个热点 key 在缓存中过期的瞬间,大量并发请求同时涌入 | 单 key 瞬时洪峰 | 数据库单点压力飙升,可能引发级联超时 |
| 缓存雪崩 | 大量 key 在同一时间段集中过期,或缓存节点宕机 | 多 key / 全集群 | 数据库被击穿,服务大面积不可用 |
2.1.1 缓存穿透
攻击者或异常请求持续查询一个数据库中也不存在的 ID(如 -1 或 99999999),缓存永远无法命中,每次请求都穿透到数据库。解决方案包括:
- 空值缓存:将数据库查询为空的 key 也写入缓存,TTL 设置较短(如 30~60 秒)。
- 布隆过滤器:在缓存层之前拦截一定不存在的 key,降低无效穿透。
调优建议: 空值缓存会占用少量内存,适合 key 空间有限且攻击频率不高的场景;布隆过滤器有误判率(可控制在 1% 以下),适合 key 空间极大且对精确性要求不苛刻的场景。
2.1.2 缓存击穿
热点 key(如热搜、爆款商品)在过期瞬间,大量并发请求同时回源查询数据库。核心解决思路是互斥锁或逻辑过期:
- 互斥锁(Mutex):只允许一个线程去数据库加载数据,其他线程等待或快速失败。
- 逻辑过期:缓存中额外存储一个过期时间字段,异步线程负责刷新,业务线程直接返回旧数据。
2.1.3 缓存雪崩
雪崩的破坏力最大。预防措施分为“预防过期”和“预防宕机”两个维度:
- 过期时间打散:在基础 TTL 上增加随机偏移(如 ±10%~20%),避免大量 key 同时过期。
- 缓存高可用:使用 Redis 哨兵或集群模式,避免单点故障。
- 限流降级:在数据库访问层增加限流,保护后端。
常见误区: 很多人把“击穿”和“雪崩”混为一谈。击穿是单 key 过期瞬间的并发问题,雪崩是多 key 集中过期或节点宕机导致的全局问题。两者的防护手段完全不同。
2.2 缓存数据同步三大模式
缓存与数据库之间的数据一致性是后端架构的核心难题。业界沉淀出三种经典模式:旁路缓存(Cache-Aside)、读写穿透(Read/Write-Through)、异步回写(Write-Behind)。
| 模式 | 读流程 | 写流程 | 一致性保证 | 适用场景 |
|---|---|---|---|---|
| 旁路缓存 | 应用先读缓存,miss 后读 DB 并回填 | 应用先更新 DB,再删除/更新缓存 | 最终一致性(依赖删除策略) | 读多写少,对一致性要求不苛刻 |
| 读写穿透 | 缓存层代理读请求,miss 时自动加载 DB | 缓存层代理写请求,同步写 DB 并更新缓存 | 强一致性(同步写) | 需要缓存与 DB 严格同步 |
| 异步回写 | 同旁路缓存或穿透 | 只写缓存,异步批量写 DB | 弱一致性(有延迟窗口) | 写吞吐极高,可接受短暂不一致 |
2.2.1 旁路缓存(Cache-Aside)
这是最常用的模式,应用代码显式管理缓存。读时先查缓存,未命中则查 DB 并回填;写时先更新 DB,然后删除缓存(而非更新),下次读时自然回填新数据。
为什么写时删除缓存而不是更新? 因为并发写场景下,更新缓存可能引入“旧数据覆盖新数据”的竞态条件。删除缓存让下次读请求触发回填,天然避免了不一致。
// 旁路缓存 - 读流程(Java 伪代码)
public String get(String key) {
String value = cache.get(key);
if (value == null) {
// 分布式锁防止击穿
value = db.query(key);
cache.set(key, value, 300); // TTL 300s
}
return value;
}
// 旁路缓存 - 写流程
public void update(String key, String newValue) {
db.update(key, newValue);
cache.del(key); // 删除缓存,而非更新
}
2.2.2 读写穿透(Read/Write-Through)
缓存层作为数据访问的唯一入口,应用不直接操作数据库。缓存组件内部封装了与 DB 的同步逻辑,对应用透明。这种模式通常需要自定义缓存中间件或使用支持该模式的缓存系统(如 Oracle Coherence)。
- Read-Through:缓存 miss 时,缓存层自动加载 DB 数据并返回。
- Write-Through:写请求先写缓存,缓存层同步写 DB,两者在同一个事务中完成。
2.2.3 异步回写(Write-Behind)
写操作只更新缓存,立即返回;缓存层异步批量合并后写入数据库。这种模式能极大提升写吞吐量,但存在数据丢失风险(缓存宕机时未落盘的数据会丢失)。
- 优点:写性能极高,适合日志、计数、埋点等场景。
- 缺点:数据一致性弱,需要配合持久化或 WAL(Write-Ahead Log)机制。
选型提醒: 金融、订单等强一致场景慎用 Write-Behind。如果必须使用,务必开启 Redis AOF 持久化(everysec 或 always 模式),并配置合理的异步刷盘间隔。
2.3 模式选型决策矩阵
在实际项目中,选择哪种模式取决于对一致性、吞吐量和实现复杂度的权衡。以下矩阵可作为选型参考:
| 业务场景 | 推荐模式 | 理由 |
|---|---|---|
| 用户信息、配置项 | 旁路缓存 | 读多写少,允许短暂不一致,实现简单 |
| 库存、余额 | 读写穿透 | 强一致性要求,写后立即读必须看到最新值 |
| 访问计数、埋点日志 | 异步回写 | 写吞吐极高,可接受秒级延迟 |
| 热点新闻、排行榜 | 旁路缓存 + 逻辑过期 | 避免击穿,允许异步刷新 |
性能调优第一原则: 缓存不是银弹。在引入缓存前,先确认数据库查询是否真的成为瓶颈;缓存只解决“读多写少”或“读负载集中”的问题,对于写密集型场景,优先考虑异步回写或消息队列削峰。