2. 缓存穿透与布隆过滤器

2.1 缓存穿透原理

缓存穿透是指查询一个根本不存在的数据时,缓存层和持久层(数据库)都无法命中。由于缓存层不命中,请求会直接穿透到数据库,当并发量较大时,数据库会承受巨大压力,甚至导致宕机。

核心特征:缓存穿透的根源在于“缓存层对不存在数据的缺失保护”。每次请求都绕过缓存直达数据库,且数据库也返回空结果,导致缓存层始终无法建立有效缓存。

缓存穿透的典型场景

  • 恶意攻击:攻击者构造大量不存在的 Key(如负数 ID、超长字符串)发起请求。
  • 业务误操作:前端或上游服务传入非法参数,导致查询数据库返回空。
  • 数据未同步:数据库已删除数据,但缓存未及时清理,后续请求穿透。

缓存穿透的危害

危害维度 具体表现 影响等级
数据库连接耗尽 大量无效查询占用连接池,正常请求被阻塞 严重
响应延迟飙升 数据库负载过高,查询响应时间从毫秒级升至秒级 严重
系统雪崩 数据库宕机后,依赖数据库的服务连锁崩溃 灾难
资源浪费 CPU、内存、I/O 被无效请求消耗 中等

常见解决方案对比

方案 原理 优点 缺点
缓存空对象 将数据库返回的 null 也缓存,设置较短 TTL 实现简单,兼容性好 占用缓存空间,TTL 内数据不一致
布隆过滤器 预先将所有可能存在的数据 Key 存入过滤器,请求先过过滤器 内存占用极低,不存在数据直接拦截 存在误判率,不支持删除
参数校验 在入口层对请求参数进行合法性校验 从源头拦截非法请求 无法拦截合法但不存在的数据
最佳实践:生产环境中通常将“布隆过滤器”与“缓存空对象”结合使用。布隆过滤器拦截大部分穿透请求,少量误判请求通过缓存空对象兜底,形成双层防护。

2.2 布隆过滤器实现

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。它由一个位数组和多个哈希函数组成。

核心数据结构

  • 位数组(Bit Array):长度为 m 的二进制数组,初始所有位为 0。
  • 哈希函数集合:k 个相互独立的哈希函数,每个函数将输入元素映射到位数组的一个位置。

基本操作流程

  1. 添加元素:对元素使用 k 个哈希函数,得到 k 个位索引,将位数组中这些位置置为 1。
  2. 查询元素:对元素使用相同的 k 个哈希函数,检查所有对应位是否都为 1。如果任意一位为 0,则元素一定不存在;如果全部为 1,则元素可能存在。
重要特性:布隆过滤器可以 100% 判断元素“不存在”,但无法 100% 判断元素“存在”。存在误判(False Positive)的可能性,即实际不存在的元素被判定为存在。
布隆过滤器添加与查询流程 输入元素 X Hash 1 Hash 2 Hash 3 Hash 4 Hash 5 Hash 6 位数组(长度 m) 0 1 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0 0 0 0 查询结果:所有位均为 1 → 可能存在(有误判风险) 查询结果:任意位为 0 → 一定不存在(100% 准确)

Java 实现示例(Guava 布隆过滤器)

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;

public class BloomFilterDemo {
    public static void main(String[] args) {
        // 预期插入数量:100000,期望误判率:1%
        BloomFilter<String> bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            100_000,
            0.01
        );

        // 添加元素
        bloomFilter.put("user_1001");
        bloomFilter.put("user_1002");
        bloomFilter.put("order_2024001");

        // 查询元素
        System.out.println(bloomFilter.mightContain("user_1001"));   // true
        System.out.println(bloomFilter.mightContain("user_9999"));   // false(大概率)
        System.out.println(bloomFilter.mightContain("order_9999"));  // false(大概率)
    }
}

2.3 布隆过滤器误判率与容量设计

布隆过滤器的性能由三个核心参数决定:位数组长度 m、哈希函数个数 k、预期元素数量 n。误判率 p 与这些参数之间存在严格的数学关系。

核心公式

  • 误判率 p:p ≈ (1 - e-kn/m)k
  • 最优哈希函数个数 k:k = (m/n) × ln2 ≈ 0.693 × (m/n)
  • 位数组长度 m:m = - (n × ln p) / (ln2)2

参数设计对照表

预期元素数 n 期望误判率 p 位数组长度 m 哈希函数个数 k 内存占用
10 万 1% (0.01) 958,505 位 ≈ 117 KB 7 极低
10 万 0.1% (0.001) 1,438,258 位 ≈ 175 KB 10 低
100 万 1% (0.01) 9,585,058 位 ≈ 1.14 MB 7 低
100 万 0.1% (0.001) 14,382,587 位 ≈ 1.72 MB 10 低
1000 万 1% (0.01) 95,850,583 位 ≈ 11.4 MB 7 中等
1 亿 1% (0.01) 958,505,834 位 ≈ 114 MB 7 较高
设计原则:
  • 误判率每降低一个数量级,位数组长度约增加 50%。
  • 哈希函数个数 k 在 7~10 之间时,空间效率最优。
  • 实际生产建议将误判率设置在 0.1%~1% 之间,兼顾内存与准确性。

容量设计步骤

  1. 确定 n:统计业务中最大可能的 Key 数量(如用户数、订单数)。
  2. 确定 p:根据业务容忍度设定误判率(通常 0.1%~1%)。
  3. 计算 m:使用公式 m = - (n × ln p) / (ln2)2 计算位数组长度。
  4. 计算 k:使用公式 k = (m/n) × ln2 计算最优哈希函数个数。
  5. 验证内存:确认 m 位对应的内存大小是否在可接受范围内。

误判率与容量关系图

误判率与位数组长度关系(n=100万) 0.1% 0.5% 1% 5% 10% 误判率 p 0 MB 5 MB 10 MB 15 MB 位数组长度 14.4 MB 9.6 MB 4.8 MB 误判率从 0.1% 放宽到 10%,位数组长度可减少约 67%
生产建议:
  • 对于用户 ID 类场景(n < 1000 万),推荐误判率 0.1%,内存占用约 1.7 MB。
  • 对于订单号类场景(n < 1 亿),推荐误判率 1%,内存占用约 11.4 MB。
  • 布隆过滤器不支持删除操作。如需删除,可考虑使用计数布隆过滤器(Counting Bloom Filter)或布谷鸟过滤器(Cuckoo Filter)。

布隆过滤器在缓存穿透中的部署架构

布隆过滤器防缓存穿透部署架构 客户端请求 布隆过滤器 Key 存在性判断 缓存层 Redis / Memcached 数据库 MySQL / PostgreSQL 拒绝(Key 不存在) 缓存空对象兜底
部署注意事项:
  • 布隆过滤器应部署在缓存层之前,作为第一道防线。
  • 过滤器中的数据需要与数据库全量同步,建议在系统启动时预热加载。
  • 对于新增数据,需同时写入数据库和布隆过滤器,保证一致性。
  • 布隆过滤器无法处理删除操作,删除数据后需重建过滤器或使用计数布隆过滤器。