2、缓存穿透保护算法总览:布隆过滤器、空值缓存、限流降级、接口校验

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都无法命中,导致每次请求都直接穿透到后端数据库。在高并发场景下,这种无效请求会瞬间压垮数据库。本章从四种主流保护算法出发,梳理其原理、适用场景与组合策略。

2.1 四种保护算法对比总览

保护算法 核心原理 拦截时机 内存开销 误判率 典型场景
布隆过滤器 位数组 + 多个哈希函数,快速判断 key 是否存在 缓存查询前 低(固定位图) 有(可接受) 海量 key 过滤、黑名单校验
空值缓存 将查询结果为空的 key 也缓存起来,设置短 TTL 缓存回源后 中(随空 key 增长) 无 热点空 key、临时性穿透
限流降级 控制单位时间请求量,超出阈值直接拒绝或返回默认值 请求入口 极低(计数器) 无 突发流量、系统过载保护
接口校验 在业务层对请求参数进行合法性校验,拦截非法 key 业务逻辑入口 无 无 参数格式校验、ID 范围校验
选型建议:布隆过滤器适合大规模 key 预判,空值缓存适合小规模热点空 key,限流降级是兜底手段,接口校验是成本最低的第一道防线。实际生产环境通常组合使用 2~3 种算法。

2.2 布隆过滤器:概率型前置过滤

布隆过滤器(Bloom Filter)由一个很长的二进制位数组和多个哈希函数组成。当插入一个 key 时,通过 k 个哈希函数计算出 k 个位索引,并将这些位全部置为 1。查询时,同样计算 k 个位索引,如果所有位都是 1,则 key 可能存在;如果任意一位为 0,则 key 一定不存在。

  • 优点:空间效率极高,插入和查询时间复杂度均为 O(k)。
  • 缺点:存在误判率(假阳性),且无法删除元素(标准实现)。
  • 工程参数:误判率 p 通常设为 1%~5%,位数组长度 m 和哈希函数个数 k 根据预估元素量 n 计算。
// 布隆过滤器核心参数估算公式(近似)
m = - (n * ln(p)) / (ln(2)^2)
k = (m / n) * ln(2)
工程案例:某电商商品详情页,商品 ID 为 9 位数字,总量约 1 亿。使用布隆过滤器(误判率 1%)仅需约 120 MB 内存,即可拦截 99% 以上的穿透请求。
布隆过滤器穿透拦截流程 请求进入 布隆过滤器判断 不存在 → 直接拒绝 位图命中 0 可能存在 → 查缓存 位图全 1,允许通过 缓存查询 缓存命中 → 返回 未命中 → 查数据库 布隆过滤器拦截了大部分不存在的 key,只有“可能存在”的请求才继续向下传递 误判的 key 会穿透到缓存层,但不会穿透到数据库(空值缓存兜底)

2.3 空值缓存:兜底回写策略

当数据库查询结果为空时,仍然将这个 key 写入缓存,但设置一个较短的过期时间(通常 30~60 秒)。后续相同 key 的请求直接返回空值,不再穿透到数据库。

  • 优点:实现简单,对现有代码侵入小,能有效拦截重复空 key 穿透。
  • 缺点:占用额外缓存空间,且无法拦截第一次穿透(第一次仍需查库)。
  • 优化方向:结合布隆过滤器使用,布隆过滤器拦截大部分,空值缓存兜底剩余误判。
// 空值缓存伪代码示例
public Object getWithNullCache(String key) {
    Object value = cache.get(key);
    if (value != null) {
        return value;  // 缓存命中,直接返回
    }
    // 缓存未命中,查数据库
    value = db.query(key);
    if (value == null) {
        // 空值也缓存,TTL 设为 30 秒
        cache.set(key, NULL_PLACEHOLDER, 30);
    } else {
        cache.set(key, value, 3600);
    }
    return value;
}

2.4 限流降级:系统级熔断保护

限流降级不直接解决穿透问题,而是防止穿透流量过大导致系统崩溃。当单位时间请求量超过阈值时,直接拒绝请求或返回降级结果(如默认值、缓存旧数据)。

限流算法 特点 适用场景
固定窗口计数器 实现简单,存在临界突变 低精度限流
滑动窗口日志 精度高,内存开销大 精确限流
漏桶算法 平滑流量,允许突发 流量整形
令牌桶算法 允许一定突发,常用 通用限流
注意:限流降级是最后一道防线,不能替代布隆过滤器和空值缓存。如果系统频繁触发限流,说明前置过滤策略需要优化。

2.5 接口校验:成本最低的第一道防线

在业务入口层对请求参数进行合法性校验,是最简单、最有效的穿透防护手段。很多穿透攻击正是利用参数漏洞(如负数 ID、超长字符串、特殊字符)来绕过缓存。

  • 常见校验规则:
    • ID 范围校验(如 ID > 0 且 ID < 最大值)
    • 参数格式校验(正则匹配、长度限制)
    • 业务规则校验(如订单状态、用户权限)
  • 优势:零内存开销,无误判,直接拦截非法请求。
  • 局限:只能拦截格式异常请求,无法拦截合法但不存在的数据。
// 接口校验示例
public Response queryProduct(String productId) {
    // 1. 参数非空校验
    if (productId == null || productId.isEmpty()) {
        return Response.fail("参数不能为空");
    }
    // 2. 格式校验:必须为纯数字,长度 6~10 位
    if (!productId.matches("\\d{6,10}")) {
        return Response.fail("参数格式错误");
    }
    // 3. 业务校验:产品 ID 必须在有效范围内
    long id = Long.parseLong(productId);
    if (id < 100000 || id > 9999999999L) {
        return Response.fail("参数超出范围");
    }
    // 通过校验,继续业务逻辑
    return processQuery(id);
}

2.6 四种算法组合策略

单一算法无法覆盖所有穿透场景,生产环境通常采用分层组合策略:

  1. 第一层(接口校验):拦截格式异常、范围异常的非法请求。
  2. 第二层(布隆过滤器):拦截大部分不存在的 key,降低缓存层压力。
  3. 第三层(空值缓存):兜底布隆过滤器的误判 key,防止穿透到数据库。
  4. 第四层(限流降级):在极端流量下保护系统整体可用性。
缓存穿透四层防护组合策略 接口校验 拦截非法参数 布隆过滤器 拦截不存在 key 空值缓存 兜底误判 key 限流降级 系统兜底 请求依次经过四层过滤,每一层拦截对应类型的穿透请求 只有通过全部四层的请求才能到达数据库,最大程度降低穿透风险 数据库
最佳实践:某社交平台 Feed 流系统采用“接口校验 + 布隆过滤器 + 空值缓存”三层组合,穿透率从 12% 降至 0.03%,数据库 QPS 下降 85%。限流降级作为应急开关,仅在极端流量下触发。