4. 缓存穿透:定义与危害、产生原因、解决方案

缓存穿透是 Redis 缓存失效场景中风险最高、最容易被忽视的一类问题。它不依赖缓存键的过期或淘汰,而是直接利用“查询不存在的数据”这一逻辑漏洞,绕过缓存层对后端存储发起持续攻击。理解其定义、危害与解决方案,是构建搜索缓存闭环的第一道防线。

4.1 定义与危害

定义:缓存穿透指客户端请求的数据在缓存中不存在(既未命中缓存),在数据库中也不存在,导致每次请求都直接穿透缓存层,直达数据库或后端存储系统。

危害:

  • 数据库压力激增:大量无效查询直接打到数据库,可能瞬间打满连接池或磁盘 IO。
  • 缓存层失效:缓存层形同虚设,无法起到保护后端的作用。
  • 系统雪崩风险:当穿透流量超过数据库承载阈值时,可能引发级联故障,导致整个搜索服务不可用。
  • 恶意攻击入口:攻击者可通过构造大量不存在的 key(如随机 ID、非法参数)实施穿透攻击。
⚠️ 典型场景:搜索系统中,用户输入不存在的商品 ID、不存在的用户昵称或非法组合条件,每次请求都会穿透缓存直达数据库。如果并发量达到 1000 QPS,数据库可能直接被打挂。

4.2 产生原因

缓存穿透的核心原因只有一个:查询的数据在缓存和数据库中均不存在。具体可分为以下两类:

原因分类 说明 示例
业务逻辑缺陷 未对请求参数做合法性校验,或未对空结果做缓存处理 查询 ID = -1 或 ID = 99999999 的商品
恶意攻击/爬虫 攻击者利用自动化工具,持续请求大量不存在的 key 遍历不存在的用户 ID 列表

下图展示了缓存穿透的完整请求链路:

缓存穿透请求链路图 客户端 Redis 缓存 数据库 ① 查询 key ② 未命中 (null) ③ 穿透查询 ④ 返回空 (数据不存在) 穿透结果 缓存与数据库均无数据 每次请求都直连数据库

4.3 解决方案

针对缓存穿透,业界有两种主流解决方案:布隆过滤器和空值缓存。两者各有优劣,实际生产环境中常组合使用。

4.3.1 布隆过滤器

原理:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由位数组和多个哈希函数组成,可以快速过滤掉一定不存在的数据。

  • 优点:内存占用极低(亿级数据仅需几百 MB),查询速度极快(O(k),k 为哈希函数个数)。
  • 缺点:存在误判率(可能将不存在的数据误判为存在),且无法删除元素(标准实现)。
  • 适用场景:数据量极大、对误判容忍度较低、数据相对静态的场景(如商品 ID 白名单)。
💡 布隆过滤器工作流程:
  1. 系统启动时,将所有合法 key(如所有商品 ID)加载到布隆过滤器中。
  2. 每次查询前,先检查布隆过滤器:如果返回“不存在”,直接拒绝请求,不再查询缓存和数据库。
  3. 如果返回“可能存在”,则继续走缓存 → 数据库的常规流程。

下图展示了布隆过滤器在缓存穿透防御中的位置:

布隆过滤器防御缓存穿透流程图 客户端 布隆过滤器 Redis 缓存 数据库 ① 查询 key ② 不存在 → 直接拒绝 ③ 可能存在 ④ 未命中 防御效果 非法 key 在第一步 即被拦截

4.3.2 空值缓存

原理:当查询数据库返回空结果时,仍然将该 key 写入缓存,但 value 设置为一个特殊标记(如 null 或 ""),并设置较短的过期时间(如 30~60 秒)。后续相同 key 的请求直接命中缓存,不再穿透到数据库。

  • 优点:实现简单,无需额外组件,对任意 key 都有效。
  • 缺点:会占用缓存空间(大量无效 key 可能挤占热点数据),且过期时间内数据可能已真实存在(短暂不一致)。
  • 适用场景:数据量中等、key 空间有限、对实时一致性要求不高的场景。
📌 空值缓存最佳实践:
  • 空值缓存的过期时间应远短于正常缓存(建议 30~60 秒),避免长期占用内存。
  • 配合 key 前缀 + 业务标识 区分空值缓存与正常缓存,方便监控和清理。
  • 在写入空值缓存时,记录日志或指标,用于识别穿透攻击源。

4.3.3 方案对比

对比维度 布隆过滤器 空值缓存
内存占用 极低(位数组) 较高(存储 key + 标记)
实现复杂度 中等(需引入额外组件) 低(代码层面即可实现)
误判率 有(可接受范围内) 无(精确缓存)
数据动态性 不适合频繁增删 适合动态数据
防御效果 拦截大部分非法 key 拦截重复非法 key

4.3.4 代码示例:空值缓存实现

// 查询商品信息(含空值缓存)
public Product getProduct(String productId) {
    // 1. 先查缓存
    String cacheKey = "product:" + productId;
    String cachedValue = redis.get(cacheKey);
    
    if (cachedValue != null) {
        // 2. 命中缓存,判断是否为空值标记
        if ("NULL_VALUE".equals(cachedValue)) {
            return null; // 数据不存在
        }
        return JSON.parseObject(cachedValue, Product.class);
    }
    
    // 3. 缓存未命中,查数据库
    Product product = productDao.selectById(productId);
    
    if (product == null) {
        // 4. 数据库也不存在,写入空值缓存(过期时间 30 秒)
        redis.setex(cacheKey, 30, "NULL_VALUE");
        return null;
    }
    
    // 5. 数据库存在,写入正常缓存(过期时间 300 秒)
    redis.setex(cacheKey, 300, JSON.toJSONString(product));
    return product;
}
✅ 生产建议:对于高并发搜索系统,推荐布隆过滤器 + 空值缓存组合使用:布隆过滤器拦截大部分非法 key,空值缓存兜底处理布隆过滤器的误判和少量合法但数据不存在的请求。同时,在网关层增加参数校验和限流,形成多层防御体系。