2. 核心概念:缓存穿透、缓存击穿、缓存雪崩的定义与区别

在构建高并发 OpenAPI 接口时,缓存层是保护数据库的第一道防线。然而,缓存层本身也存在三类经典故障场景——缓存穿透、缓存击穿和缓存雪崩。这三者虽然名称相似,但成因、影响范围和解决方案截然不同。理解它们的本质区别,是设计可靠缓存架构的前提。

2.1 缓存穿透

定义:查询一个根本不存在的数据。由于缓存中不存在该数据(因为数据本身就不存在),每次请求都会穿透缓存,直接打到数据库。如果攻击者利用大量不存在的 key 发起请求,数据库将承受巨大压力,甚至宕机。

核心特征:缓存和数据库中都没有该数据,请求直接穿透缓存层。

典型场景

  • 恶意攻击:遍历不存在的用户 ID 或商品 ID。
  • 程序 bug:查询条件错误,导致 key 格式异常。
  • 数据删除后未同步:数据库记录已删除,但缓存未及时清理。

解决方案

  • 缓存空对象:即使数据库返回空结果,也缓存一个短生命周期的空值(如 null 或特殊标记),避免重复穿透。
  • 布隆过滤器:在缓存层之前加一层布隆过滤器,快速判断 key 是否可能存在。如果过滤器判定不存在,直接拒绝请求。
  • 参数校验:在 API 入口层对请求参数进行合法性校验,拦截明显无效的查询。
缓存穿透流程示意图 客户端请求 缓存层 数据库 空值缓存 布隆过滤器 穿透后果 数据库压力激增 可能引发宕机

2.2 缓存击穿

定义:一个热点 key 在缓存中过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库。由于是热点数据,数据库瞬间承受高并发查询,可能导致服务雪崩。

核心特征:缓存中某个热点 key 失效,大量请求同时打到数据库。

典型场景

  • 秒杀商品详情页:某个爆款商品的缓存刚好过期,大量用户同时刷新。
  • 热门新闻/公告:缓存过期瞬间,所有用户请求同时穿透。
  • 定时任务失效:缓存刷新周期与高并发窗口重合。

解决方案

  • 互斥锁:当缓存失效时,只允许一个线程去数据库加载数据,其他线程等待或返回旧缓存。
  • 逻辑过期:缓存中设置逻辑过期时间,后台异步刷新,避免热点 key 同时失效。
  • 永不过期 + 主动更新:热点 key 不设置物理过期时间,由后台任务定期更新。
缓存击穿与缓存雪崩对比示意图 缓存击穿 热点 Key 缓存过期 大量并发 数据库 单个热点 key 失效 并发穿透数据库 缓存雪崩 大量 Key 同时过期 海量请求 数据库 大量 key 同时失效 数据库被压垮

2.3 缓存雪崩

定义:缓存层中大量 key 同时过期,或者缓存节点宕机,导致大量请求同时穿透到数据库。与击穿不同,雪崩影响的是全局,而非单个热点 key。

核心特征:缓存层大面积失效,数据库被海量请求淹没。

典型场景

  • 缓存服务重启:所有缓存数据清空,瞬间流量全部打到数据库。
  • 批量设置相同过期时间:大量 key 在同一时刻过期。
  • 缓存节点宕机:Redis 集群中某个分片不可用,导致该分片上的所有 key 失效。

解决方案

  • 过期时间随机化:在基础过期时间上增加随机偏移,避免大量 key 同时过期。
  • 缓存高可用:使用 Redis 哨兵或集群模式,避免单点故障。
  • 多级缓存:本地缓存(如 Caffeine)+ 分布式缓存(如 Redis)组合,降低单层失效风险。
  • 限流降级:在数据库层前加限流组件,保护数据库不被突发流量冲垮。

2.4 三者对比总结

维度 缓存穿透 缓存击穿 缓存雪崩
根本原因 查询不存在的数据 热点 key 过期 大量 key 同时过期或缓存宕机
影响范围 单个 key 或少量 key 单个热点 key 全局或大面积 key
数据库压力 持续低频或高频穿透 瞬间高并发 海量并发
典型攻击 恶意遍历不存在的 ID 热点数据失效瞬间 缓存服务重启或批量过期
核心解决方案 空值缓存 / 布隆过滤器 互斥锁 / 逻辑过期 过期随机化 / 多级缓存 / 限流
架构建议:在实际 OpenAPI 接口设计中,这三种场景往往同时存在。建议在缓存层统一实现空值缓存、热点 key 互斥锁、过期时间随机化三项基础防护,再根据业务特点叠加布隆过滤器或限流组件。

2.5 代码示例:空值缓存与互斥锁

以下是一个简化的 Java 伪代码示例,展示如何同时处理缓存穿透和缓存击穿:

public String getData(String key) {
    // 1. 查缓存
    String value = redis.get(key);
    if (value != null) {
        return value;
    }

    // 2. 缓存穿透防护:空值标记
    if (redis.exists("null_flag:" + key)) {
        return null; // 直接返回空,避免穿透
    }

    // 3. 缓存击穿防护:互斥锁
    String lockKey = "lock:" + key;
    boolean locked = redis.setnx(lockKey, "1", 3); // 3秒超时
    if (!locked) {
        Thread.sleep(50);
        return redis.get(key); // 重试或返回旧值
    }

    try {
        // 4. 查数据库
        value = database.query(key);
        if (value == null) {
            // 缓存空对象,过期时间较短
            redis.set("null_flag:" + key, "1", 60);
        } else {
            redis.set(key, value, 300);
        }
        return value;
    } finally {
        redis.del(lockKey);
    }
}
注意:互斥锁方案会增加缓存层的复杂度,且存在死锁风险。生产环境中建议使用 Redisson 等成熟分布式锁框架,并设置合理的锁超时时间。