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 集中过期或节点宕机导致的全局问题。两者的防护手段完全不同。
三大缓存失效问题对比 缓存穿透 查询不存在的数据 • 攻击者构造非法 key • 缓存永远不命中 • 数据库连接耗尽 解决方案: ① 空值缓存(短 TTL) ② 布隆过滤器拦截 缓存击穿 热点 key 过期瞬间 • 单个热点 key • 过期瞬间大量并发 • 数据库单点压力 解决方案: ① 互斥锁(Mutex) ② 逻辑过期 + 异步刷新 缓存雪崩 大量 key 集中过期 / 宕机 • 多 key 同时过期 • 缓存节点宕机 • 数据库被击穿 解决方案: ① TTL 打散 + 随机偏移 ② 缓存高可用(哨兵/集群) 核心区别:穿透 → 数据不存在;击穿 → 单 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 模式),并配置合理的异步刷盘间隔。
三种缓存同步模式架构对比 旁路缓存 应用管理缓存与 DB 应用 缓存 数据库 读/写 读/写 读写穿透 缓存层代理所有访问 应用 缓存代理层 数据库 只访问缓存 同步写 DB 异步回写 异步批量写 DB 应用 缓存(主存储) 数据库(从存储) 只写缓存 异步批量写 一致性强度:读写穿透(强) > 旁路缓存(最终) > 异步回写(弱)

2.3 模式选型决策矩阵

在实际项目中,选择哪种模式取决于对一致性、吞吐量和实现复杂度的权衡。以下矩阵可作为选型参考:

业务场景 推荐模式 理由
用户信息、配置项 旁路缓存 读多写少,允许短暂不一致,实现简单
库存、余额 读写穿透 强一致性要求,写后立即读必须看到最新值
访问计数、埋点日志 异步回写 写吞吐极高,可接受秒级延迟
热点新闻、排行榜 旁路缓存 + 逻辑过期 避免击穿,允许异步刷新
性能调优第一原则: 缓存不是银弹。在引入缓存前,先确认数据库查询是否真的成为瓶颈;缓存只解决“读多写少”或“读负载集中”的问题,对于写密集型场景,优先考虑异步回写或消息队列削峰。