2. 缓存穿透保护算法总览:布隆过滤器、空值缓存、参数校验、限流降级等方案对比

缓存穿透是指查询一个根本不存在的数据,缓存层无法命中,请求直接穿透到后端存储(如数据库、磁盘),导致存储层压力骤增。若攻击者利用大量不存在的 key 发起请求,可能直接打垮数据库。本章对四种主流穿透保护方案进行系统性对比,帮助你在不同场景下做出正确选型。

核心原则:穿透保护的目标不是“完全避免穿透”,而是“将穿透流量控制在后端可承受的范围内”。没有银弹,只有组合策略。

2.1 四种方案核心原理

方案 核心原理 拦截层级 内存开销 误判率
布隆过滤器 位数组 + 多个哈希函数,判断 key 是否可能存在 缓存层之前 低(固定位图) 有(可控制)
空值缓存 将不存在的 key 缓存一个短 TTL 的空值标记 缓存层内部 中(随空 key 数量增长) 无
参数校验 在入口层校验请求参数合法性(如 ID 格式、范围) 网关 / API 层 无 无
限流降级 对同一 IP / 用户 / key 的请求频率进行限制 网关 / 应用层 低(计数器) 无
选型提示:布隆过滤器适合“key 全集可预知”的场景(如用户 ID 范围固定);空值缓存适合“key 空间无限但穿透量可控”的场景;参数校验是成本最低的第一道防线;限流降级是兜底手段,防止恶意流量打穿所有保护。

2.2 布隆过滤器(Bloom Filter)

布隆过滤器使用一个长度为 m 的位数组和 k 个独立的哈希函数。插入 key 时,将 k 个哈希位置置为 1;查询时,若所有 k 个位置均为 1,则 key 可能存在;若任一位置为 0,则 key 一定不存在。

  • 优点:空间效率极高(百万级 key 仅需几 MB),查询时间复杂度 O(k),支持高并发。
  • 缺点:存在误判率(假阳性),无法删除元素(标准版),需要提前知道 key 全集。
  • 关键参数:位数组大小 m、哈希函数个数 k、预期元素数量 n。误判率 p ≈ (1 - e-kn/m)k。
// 布隆过滤器核心参数估算(Java Guava 实现)
int expectedInsertions = 1_000_000;   // 预期插入量 n
double fpp = 0.01;                    // 期望误判率 p
BloomFilter<String> bloomFilter = BloomFilter.create(
    Funnels.stringFunnel(Charset.defaultCharset()),
    expectedInsertions,
    fpp);
// 内部自动计算最优 m 和 k
避坑:布隆过滤器无法处理“冷 key 预热”问题——如果业务 key 是动态生成的(如用户自定义短链接),无法提前加载到过滤器中,此时布隆过滤器会误杀所有合法新 key。解决方案:配合空值缓存或使用计数型布隆过滤器(Counting Bloom Filter)。
布隆过滤器工作原理 布隆过滤器:插入与查询流程 0 1 0 1 0 0 1 0 0 1 0 0 1 0 0 插入 key="user:1001" Hash1 → 位2 Hash2 → 位4 Hash3 → 位7 查询 key="user:9999" Hash1 → 位1=0 一定不存在 → 直接拒绝 位数组大小 m=16,哈希函数 k=3,误判率 p≈1.2%(当 n=4 时)

2.3 空值缓存(Null Cache)

当查询一个不存在的 key 时,将 key 写入缓存,value 设为空值标记(如 null 或 "__EMPTY__"),并设置一个较短的 TTL(如 30~60 秒)。后续相同 key 的请求在 TTL 内直接返回空值,不再穿透到数据库。

  • 优点:实现简单,无误判,支持动态 key 空间。
  • 缺点:占用缓存空间(大量空 key 可能撑爆内存),TTL 设置不当会导致数据不一致(真实数据写入后仍返回空值)。
  • 关键参数:空值 TTL、空值最大数量限制、空值淘汰策略。
// 空值缓存伪代码(Redis + Lua 脚本)
String cacheKey = "user:" + userId;
String value = redis.get(cacheKey);
if (value != null) {
    if ("__EMPTY__".equals(value)) {
        return null;  // 空值命中,直接返回
    }
    return deserialize(value);
}
// 缓存未命中,查询数据库
User user = userDao.findById(userId);
if (user == null) {
    // 设置空值缓存,TTL=30秒
    redis.setex(cacheKey, 30, "__EMPTY__");
    return null;
}
redis.setex(cacheKey, 3600, serialize(user));
return user;
避坑:空值缓存容易被恶意攻击者利用——构造大量随机不存在的 key,导致缓存中塞满空值标记,挤占正常缓存空间。必须配合空值数量上限和LRU 淘汰策略,或对同一 IP 的空 key 生成速率进行限流。

2.4 参数校验与限流降级

参数校验是第一道物理防线。例如用户 ID 必须是正整数且不超过系统最大用户数,订单号必须符合特定格式。校验不通过直接返回 400 错误,不进入缓存层。

限流降级是最后一道防线。当检测到某个 key、用户或 IP 的请求频率超过阈值时,直接拒绝或返回降级结果(如默认值、静态页)。常用算法:令牌桶、漏桶、滑动窗口。

维度 参数校验 限流降级
拦截对象 非法参数 高频请求
资源消耗 几乎为零 低(计数器)
误杀风险 低(规则明确) 中(阈值设置不当)
典型实现 正则、范围检查、白名单 Guava RateLimiter、Redis 滑动窗口
适用场景 所有入口请求 恶意攻击、突发流量

2.5 方案组合策略

单一方案无法应对所有穿透场景。推荐分层组合架构:

  1. L1 参数校验:在网关层过滤明显非法的请求(如 ID ≤ 0、格式错误)。
  2. L2 布隆过滤器:拦截大部分不存在的 key(需提前加载合法 key 全集)。
  3. L3 空值缓存:兜住布隆过滤器误判和动态 key 的穿透。
  4. L4 限流降级:在应用层对同一 key 或 IP 的穿透频率进行限制。
四层穿透保护架构 缓存穿透保护:四层组合架构 客户端请求 L1 参数校验(网关层) L2 布隆过滤器(缓存层前) L3 空值缓存(缓存层) L4 限流降级(应用层) 各层拦截效果 L1: 过滤格式非法请求 L2: 拦截 99%+ 不存在 key L3: 兜住误判 + 动态 key L4: 限制穿透频率 最终穿透到 DB 的流量 可控制在 0.1% 以下

2.6 方案对比总结

对比维度 布隆过滤器 空值缓存 参数校验 限流降级
实现复杂度 中 低 低 中
内存开销 低(固定) 中(随空 key 增长) 无 低
误判/误杀 有(假阳性) 无 无 有(阈值误杀)
动态 key 支持 差(需预加载) 好 好 好
抗恶意攻击 强 弱(需配合限流) 中 强
推荐场景 key 全集固定 动态 key + 低频穿透 所有入口 高并发 + 恶意流量
实战建议:在大多数业务系统中,推荐“参数校验 + 布隆过滤器 + 空值缓存”三层组合。布隆过滤器拦截 99% 的穿透,空值缓存兜住剩余 1% 的误判和动态 key,参数校验作为前置过滤。如果系统面临 DDoS 级别的攻击,再叠加限流降级作为第四层。