2. 缓存穿透与布隆过滤器:缓存穿透原理、布隆过滤器实现、缓存空对象策略、布隆过滤器误判率控制
2.1 缓存穿透原理与危害
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,导致每次请求都直接穿透缓存层,打到数据库层。由于缓存无法命中,所有请求都变成数据库查询,当并发量较大时,数据库会因承受巨大压力而崩溃。
核心危害: 缓存层完全失效,数据库成为唯一防御屏障,极易引发雪崩效应。
| 对比维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 数据存在性 | 数据不存在 | 数据存在但过期 | 大量数据同时过期 |
| 影响范围 | 单个key或恶意攻击 | 单个热点key | 大量key |
| 数据库压力 | 持续穿透 | 瞬时高并发 | 突发洪峰 |
| 典型场景 | 查询不存在的ID | 秒杀商品过期 | 缓存服务重启 |
2.2 布隆过滤器原理与实现
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。它由一个位数组和多个哈希函数组成,核心思想是:如果布隆过滤器说元素不存在,那它一定不存在;如果说存在,则可能存在(有误判率)。
2.3 布隆过滤器实现示例
以下是一个基于 Java 的轻量级布隆过滤器实现,使用 BitSet 和 MurmurHash 算法:
public class BloomFilter<T> {
private final BitSet bitSet;
private final int bitSetSize;
private final int numHashFunctions;
private final int[] seeds;
public BloomFilter(int expectedInsertions, double falsePositiveRate) {
// 计算最优位数组大小 m 和哈希函数个数 k
this.bitSetSize = (int) (-expectedInsertions * Math.log(falsePositiveRate) / (Math.log(2) * Math.log(2)));
this.numHashFunctions = (int) (bitSetSize / expectedInsertions * Math.log(2));
this.bitSet = new BitSet(bitSetSize);
this.seeds = new int[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
seeds[i] = i + 1;
}
}
public void add(T value) {
byte[] bytes = value.toString().getBytes(StandardCharsets.UTF_8);
for (int seed : seeds) {
int hash = murmurHash3(bytes, seed);
int index = Math.abs(hash % bitSetSize);
bitSet.set(index, true);
}
}
public boolean mightContain(T value) {
byte[] bytes = value.toString().getBytes(StandardCharsets.UTF_8);
for (int seed : seeds) {
int hash = murmurHash3(bytes, seed);
int index = Math.abs(hash % bitSetSize);
if (!bitSet.get(index)) {
return false; // 一定不存在
}
}
return true; // 可能存在
}
private int murmurHash3(byte[] data, int seed) {
// MurmurHash3 实现(略)
return 0;
}
}
2.4 缓存空对象策略
缓存空对象(Cache Null Object)是解决缓存穿透的另一种常用策略。当查询数据库发现数据不存在时,将一个空值(如 null 或特殊标记)也写入缓存,并设置较短的过期时间。
| 对比维度 | 布隆过滤器 | 缓存空对象 |
|---|---|---|
| 空间占用 | 极低(位数组) | 较高(缓存 key-value) |
| 误判风险 | 有误判率(可控制) | 无误判 |
| 实现复杂度 | 中等 | 简单 |
| 内存回收 | 不支持删除 | 支持 TTL 自动过期 |
| 适用场景 | 大量不存在的 key 攻击 | 少量不存在的 key 查询 |
最佳实践: 将布隆过滤器作为前置过滤层,缓存空对象作为兜底策略。布隆过滤器拦截大部分穿透请求,缓存空对象处理少量漏网之鱼。
2.5 布隆过滤器误判率控制
布隆过滤器的误判率(False Positive Rate)由三个参数决定:位数组大小 m、哈希函数个数 k、已插入元素数量 n。误判率公式为:
P ≈ (1 - e-kn/m)k
在实际工程中,通过以下方式控制误判率:
- 增大位数组 m: 位数组越大,误判率越低,但内存占用增加。
- 优化哈希函数 k: 最优哈希函数个数 k = (m/n) * ln2,过多或过少都会增加误判率。
- 限制插入数量 n: 预估最大元素数量,超出后误判率急剧上升。
2.6 缓存穿透防御组合策略
在实际生产环境中,单一策略往往不足以应对复杂攻击场景,推荐采用多层防御体系:
- 第一层:参数校验 — 拒绝明显不合法的请求(如 ID <= 0、超长字符串)。
- 第二层:布隆过滤器 — 拦截大部分不存在的 key,降低数据库查询量。
- 第三层:缓存空对象 — 对布隆过滤器误判的请求进行兜底,设置短 TTL(如 30-60 秒)。
- 第四层:限流与熔断 — 当数据库压力超过阈值时,触发限流保护。
安全合规提示: 缓存空对象策略中,空值缓存可能被恶意利用进行内存耗尽攻击。建议对空值缓存设置独立的内存上限和更短的过期时间,并记录异常请求日志用于安全审计。