2. 缓存穿透解决方案:布隆过滤器原理、空值缓存策略、参数校验与限流

缓存穿透是指查询一个根本不存在的数据,缓存层和数据库层都无法命中,导致每次请求都直接打到数据库。在高并发场景下,这会将数据库压垮。本节深入剖析三种主流解决方案的原理、适用场景与协同策略。

2.1 缓存穿透的成因与危害

穿透的本质是缓存层失效与数据不存在的叠加效应。攻击者或异常流量利用这一点,构造大量不存在的 Key 发起请求,绕过缓存直接穿透到数据库。

维度 说明
触发条件 查询 Key 在缓存中不存在,且数据库中也不存在该记录
直接后果 每次请求都穿透缓存,数据库连接数飙升,响应延迟剧增
典型场景 恶意攻击(遍历不存在的 ID)、业务漏洞(前端未做参数校验)
危害等级 高——可导致数据库连接池耗尽、服务雪崩
关键认知:缓存穿透与缓存击穿(热点 Key 过期)不同。穿透是数据本身不存在,击穿是数据存在但缓存刚好过期。治理策略完全不同,不可混淆。

2.2 布隆过滤器原理与实现

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它允许一定的误判率(假阳性),但绝不漏判(假阴性概率为 0)。

2.2.1 核心原理

  1. 位数组:初始化一个长度为 m 的二进制位数组,所有位初始为 0。
  2. 多个哈希函数:使用 k 个相互独立的哈希函数,每个函数将输入元素映射到位数组的某个位置。
  3. 插入操作:对每个元素,用 k 个哈希函数计算得到 k 个位置,将这些位置全部置为 1。
  4. 查询操作:对查询元素,同样计算 k 个哈希位置。如果所有位置都是 1,则元素可能存在;如果任意一个位置是 0,则元素一定不存在。
数学特性:误判率 p 与位数组长度 m、哈希函数个数 k、插入元素数量 n 相关。最优哈希函数个数 k = (m/n) * ln2。实际工程中,通常将误判率控制在 1% 以内。
布隆过滤器插入与查询流程 布隆过滤器:插入与查询流程 输入元素 X Hash₁(X) Hash₂(X) ... Hashₖ(X) 位数组(长度 m) [0,1,0,1,1,0,1,0,0,1,1,0,1,0,0,1] 插入成功 查询元素 Y Hash₁(Y) ... Hashₖ(Y) 检查 k 个位置是否全为 1 一定不存在(拒绝) 可能存在(放行)

2.2.2 工程实现要点

参数 推荐值 说明
位数组长度 m 预估数据量 n × 10 ~ 20 越大误判率越低,但内存占用增加
哈希函数个数 k 7 ~ 13 根据 m/n 计算最优值
误判率 p ≤ 1% 业务可接受范围,过高会失去过滤意义
数据结构 Redis Bitmap / Guava BloomFilter 分布式场景用 Redis,单机用 Guava
// Java 示例:使用 Guava 布隆过滤器
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

// 预估插入 100 万条数据,误判率 1%
BloomFilter<Integer> bloomFilter = BloomFilter.create(
    Funnels.integerFunnel(), 1_000_000, 0.01);

// 插入所有合法 ID
for (Integer id : validIdList) {
    bloomFilter.put(id);
}

// 查询时先过布隆过滤器
public Object queryWithBloom(String key) {
    if (!bloomFilter.mightContain(key)) {
        // 一定不存在,直接返回空
        return null;
    }
    // 可能存在,继续查缓存 + 数据库
    return queryFromCacheOrDB(key);
}
实践建议:布隆过滤器无法删除元素。如果业务需要频繁删除,考虑使用计数布隆过滤器(Counting Bloom Filter)或布谷鸟过滤器(Cuckoo Filter),后者支持删除且空间效率更高。

2.3 空值缓存策略

空值缓存(Cache-Through for Null)是最简单直接的穿透治理手段:当数据库查询结果为空时,仍然将该 Key 写入缓存,但 Value 标记为空值占位符,并设置较短的过期时间。

2.3.1 实现流程

  1. 请求到达,先查缓存。
  2. 缓存命中且值为空占位符,直接返回空(避免查库)。
  3. 缓存未命中,查数据库。
  4. 数据库结果为空,将 Key 写入缓存,Value 设为 NULL_PLACEHOLDER,TTL 设为 30~60 秒。
  5. 数据库结果非空,正常写入缓存并返回。
// 空值缓存伪代码
public Object queryWithNullCache(String key) {
    // 1. 查缓存
    Object cacheValue = cache.get(key);
    if (cacheValue != null) {
        if (NULL_PLACEHOLDER.equals(cacheValue)) {
            return null; // 空值命中,直接返回
        }
        return cacheValue;
    }
    // 2. 缓存未命中,查数据库
    Object dbValue = db.query(key);
    if (dbValue == null) {
        // 3. 空值缓存,TTL 设为 30 秒
        cache.set(key, NULL_PLACEHOLDER, 30);
        return null;
    }
    // 4. 正常缓存
    cache.set(key, dbValue, 3600);
    return dbValue;
}

2.3.2 优缺点对比

维度 布隆过滤器 空值缓存
空间开销 极低(位数组) 较高(每个空 Key 占用完整缓存条目)
误判风险 有(假阳性) 无(精确判断)
实现复杂度 中等(需维护哈希函数) 低(简单 if-else)
适用场景 海量 Key、内存敏感 Key 数量可控、对精确性要求高
删除支持 困难(标准布隆不支持) 容易(直接删除缓存条目)
避坑指南:空值缓存的 TTL 不宜过长(建议 ≤ 60 秒),否则当真实数据写入后,缓存中仍然是空占位符,导致数据不一致。如果业务对一致性要求高,可在数据写入时主动删除空值缓存。

2.4 参数校验与限流

参数校验和限流是第一道防线,在请求进入缓存层之前就拦截非法流量。

2.4.1 参数校验策略

  • 格式校验:ID 必须是正整数、UUID 格式、长度范围等。
  • 业务规则校验:如订单号必须包含日期前缀、用户 ID 必须在合法范围内。
  • 黑名单校验:对已知的恶意 Key 模式(如 ../../etc/passwd)直接拒绝。
// 参数校验示例
public boolean validateKey(String key) {
    // 1. 非空校验
    if (key == null || key.trim().isEmpty()) return false;
    // 2. 格式校验:必须是数字
    if (!key.matches("\\d+")) return false;
    // 3. 范围校验:ID 必须在 1 ~ 1亿 之间
    long id = Long.parseLong(key);
    if (id < 1 || id > 100_000_000) return false;
    // 4. 黑名单校验
    if (BLACKLIST.contains(key)) return false;
    return true;
}

2.4.2 限流策略

限流用于控制单位时间内的请求速率,防止突发穿透流量打垮数据库。常用算法包括:

算法 特点 适用场景
令牌桶 允许突发流量,平滑速率 通用场景,推荐首选
漏桶 强制固定速率,不允许突发 对稳定性要求极高的场景
滑动窗口 精确控制时间窗口内的请求数 需要精确计数的场景
缓存穿透三层防护架构 缓存穿透三层防护架构 第一层:入口防护 参数校验 限流(令牌桶) 黑名单过滤 第二层:布隆过滤器 判断 Key 是否可能存在(假阳性率 ≤ 1%) 第三层:空值缓存 数据库空结果写入缓存(TTL 30~60s) 数据库(最终屏障)

2.5 三种方案协同策略

在实际生产系统中,三种方案通常组合使用,形成纵深防御体系:

  1. 第一道防线(入口层):参数校验 + 限流,拦截明显非法的请求。
  2. 第二道防线(缓存层前置):布隆过滤器,快速过滤掉绝大多数不存在的 Key。
  3. 第三道防线(缓存层内部):空值缓存,兜底处理布隆过滤器的假阳性以及少量合法但数据不存在的请求。
推荐组合:布隆过滤器 + 空值缓存 + 限流。布隆过滤器拦截 99% 的不存在 Key,空值缓存处理剩余 1% 的假阳性,限流防止突发流量。三者配合可将数据库穿透请求降低 99.99% 以上。

2.6 本章小结

  • 布隆过滤器:空间高效,适合海量 Key 场景,但存在假阳性且不支持删除。
  • 空值缓存:实现简单,精确判断,但占用缓存空间,需注意 TTL 设置。
  • 参数校验与限流:第一道防线,成本最低,必须前置。
  • 协同策略:三层防护层层递进,缺一不可。