2. 缓存穿透与布隆过滤器
2.1 缓存穿透原理
缓存穿透是指查询一个根本不存在的数据时,缓存层和持久层(数据库)都无法命中。由于缓存层不命中,请求会直接穿透到数据库,当并发量较大时,数据库会承受巨大压力,甚至导致宕机。
核心特征:缓存穿透的根源在于“缓存层对不存在数据的缺失保护”。每次请求都绕过缓存直达数据库,且数据库也返回空结果,导致缓存层始终无法建立有效缓存。
缓存穿透的典型场景
- 恶意攻击:攻击者构造大量不存在的 Key(如负数 ID、超长字符串)发起请求。
- 业务误操作:前端或上游服务传入非法参数,导致查询数据库返回空。
- 数据未同步:数据库已删除数据,但缓存未及时清理,后续请求穿透。
缓存穿透的危害
| 危害维度 | 具体表现 | 影响等级 |
|---|---|---|
| 数据库连接耗尽 | 大量无效查询占用连接池,正常请求被阻塞 | 严重 |
| 响应延迟飙升 | 数据库负载过高,查询响应时间从毫秒级升至秒级 | 严重 |
| 系统雪崩 | 数据库宕机后,依赖数据库的服务连锁崩溃 | 灾难 |
| 资源浪费 | CPU、内存、I/O 被无效请求消耗 | 中等 |
常见解决方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 缓存空对象 | 将数据库返回的 null 也缓存,设置较短 TTL | 实现简单,兼容性好 | 占用缓存空间,TTL 内数据不一致 |
| 布隆过滤器 | 预先将所有可能存在的数据 Key 存入过滤器,请求先过过滤器 | 内存占用极低,不存在数据直接拦截 | 存在误判率,不支持删除 |
| 参数校验 | 在入口层对请求参数进行合法性校验 | 从源头拦截非法请求 | 无法拦截合法但不存在的数据 |
最佳实践:生产环境中通常将“布隆过滤器”与“缓存空对象”结合使用。布隆过滤器拦截大部分穿透请求,少量误判请求通过缓存空对象兜底,形成双层防护。
2.2 布隆过滤器实现
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。它由一个位数组和多个哈希函数组成。
核心数据结构
- 位数组(Bit Array):长度为 m 的二进制数组,初始所有位为 0。
- 哈希函数集合:k 个相互独立的哈希函数,每个函数将输入元素映射到位数组的一个位置。
基本操作流程
- 添加元素:对元素使用 k 个哈希函数,得到 k 个位索引,将位数组中这些位置置为 1。
- 查询元素:对元素使用相同的 k 个哈希函数,检查所有对应位是否都为 1。如果任意一位为 0,则元素一定不存在;如果全部为 1,则元素可能存在。
重要特性:布隆过滤器可以 100% 判断元素“不存在”,但无法 100% 判断元素“存在”。存在误判(False Positive)的可能性,即实际不存在的元素被判定为存在。
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% 之间,兼顾内存与准确性。
容量设计步骤
- 确定 n:统计业务中最大可能的 Key 数量(如用户数、订单数)。
- 确定 p:根据业务容忍度设定误判率(通常 0.1%~1%)。
- 计算 m:使用公式 m = - (n × ln p) / (ln2)2 计算位数组长度。
- 计算 k:使用公式 k = (m/n) × ln2 计算最优哈希函数个数。
- 验证内存:确认 m 位对应的内存大小是否在可接受范围内。
误判率与容量关系图
生产建议:
- 对于用户 ID 类场景(n < 1000 万),推荐误判率 0.1%,内存占用约 1.7 MB。
- 对于订单号类场景(n < 1 亿),推荐误判率 1%,内存占用约 11.4 MB。
- 布隆过滤器不支持删除操作。如需删除,可考虑使用计数布隆过滤器(Counting Bloom Filter)或布谷鸟过滤器(Cuckoo Filter)。
布隆过滤器在缓存穿透中的部署架构
部署注意事项:
- 布隆过滤器应部署在缓存层之前,作为第一道防线。
- 过滤器中的数据需要与数据库全量同步,建议在系统启动时预热加载。
- 对于新增数据,需同时写入数据库和布隆过滤器,保证一致性。
- 布隆过滤器无法处理删除操作,删除数据后需重建过滤器或使用计数布隆过滤器。