2. 缓存穿透:定义与成因、空值缓存与布隆过滤器原理、布隆过滤器代码实现示例
2.1 缓存穿透的定义与成因
缓存穿透是指客户端请求的数据在缓存中不存在,在数据库中也不存在,导致每次请求都直接穿透缓存层,打到后端数据库。由于缓存层无法命中,数据库也返回空结果,这类请求会持续绕过缓存,造成数据库压力陡增,甚至引发雪崩。
核心特征:缓存未命中 + 数据库无记录 = 每次请求都直击数据库。
常见成因:
- 恶意攻击:攻击者构造大量不存在的 Key(如负 ID、超长字符串、随机组合),绕过缓存直接查询数据库。
- 业务逻辑缺陷:用户查询非法参数或已删除的资源,后端未做前置校验。
- 缓存与数据库数据不一致:数据已被删除但缓存未及时失效,导致请求穿透后数据库返回空。
- 大量并发请求:热点 Key 失效瞬间,大量请求同时穿透,数据库瞬间承压。
| 对比维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 目标 Key | 不存在的 Key | 单个热点 Key 失效 | 大量 Key 同时失效 |
| 数据库影响 | 每次请求都穿透 | 瞬间高并发穿透 | 大面积请求穿透 |
| 典型场景 | 恶意攻击、非法参数 | 热点数据过期 | 缓存宕机或批量过期 |
| 防护手段 | 空值缓存、布隆过滤器 | 互斥锁、逻辑过期 | 过期时间打散、降级限流 |
经验法则:缓存穿透是“查无此人”型攻击,防护重点在于前置过滤,而非后置兜底。
2.2 空值缓存原理
空值缓存是最直接的穿透防护手段:当数据库查询返回空结果时,仍然将该 Key 写入缓存,但 Value 标记为 null 或特殊占位符,并设置较短的过期时间(如 30~60 秒)。后续相同 Key 的请求直接命中缓存空值,不再穿透数据库。
实现要点:
- 空值缓存过期时间不宜过长,避免业务数据恢复后缓存仍返回空。
- 需要区分“缓存空值”与“缓存数据不存在”,防止误判。
- 对恶意攻击的随机 Key 无效——攻击者每次使用不同 Key,空值缓存无法覆盖。
// 伪代码示例:空值缓存
public Object getData(String key) {
Object cacheValue = cache.get(key);
if (cacheValue != null) {
// 命中缓存,直接返回(包括空值标记)
return cacheValue;
}
// 缓存未命中,查询数据库
Object dbValue = db.query(key);
if (dbValue == null) {
// 数据库无记录,缓存空值,过期时间 30 秒
cache.set(key, NULL_PLACEHOLDER, 30);
return null;
}
// 数据库有记录,写入缓存
cache.set(key, dbValue, 300);
return dbValue;
}
注意:空值缓存只能防御“有限 Key 集合”的穿透,无法应对海量随机 Key 攻击。此时需要布隆过滤器进行前置过滤。
2.3 布隆过滤器原理
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由二进制位数组和多个哈希函数组成,核心特性:
- 不存在一定不存在:如果布隆过滤器判断元素不在集合中,则一定不在。
- 存在不一定存在:存在一定的误判率(False Positive),但不会漏判(False Negative)。
工作原理:
- 初始化一个长度为 m 的位数组,所有位初始为 0。
- 添加元素时,使用 k 个哈希函数分别计算哈希值,将对应位数组位置置为 1。
- 查询元素时,同样计算 k 个哈希值,检查对应位是否全部为 1:
- 如果任意一位为 0,则元素一定不存在。
- 如果全部为 1,则元素可能存在(有误判概率)。
2.4 布隆过滤器代码实现示例
以下是一个基于 Java 的简易布隆过滤器实现,使用 BitSet 和多个哈希函数:
import java.util.BitSet;
public class SimpleBloomFilter {
private static final int DEFAULT_SIZE = 2 << 24; // 约 3355 万位
private static final int[] SEEDS = {3, 7, 11, 13, 31, 37, 61};
private BitSet bits = new BitSet(DEFAULT_SIZE);
private HashFunction[] funcs = new HashFunction[SEEDS.length];
public SimpleBloomFilter() {
for (int i = 0; i < SEEDS.length; i++) {
funcs[i] = new HashFunction(DEFAULT_SIZE, SEEDS[i]);
}
}
// 添加元素
public void add(String value) {
for (HashFunction f : funcs) {
bits.set(f.hash(value), true);
}
}
// 判断元素是否存在
public boolean mightContain(String value) {
if (value == null) return false;
for (HashFunction f : funcs) {
if (!bits.get(f.hash(value))) {
return false; // 某位为 0,一定不存在
}
}
return true; // 全部为 1,可能存在
}
// 哈希函数内部类
public static class HashFunction {
private int cap;
private int seed;
public HashFunction(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
for (int i = 0; i < value.length(); i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
// 使用示例
public static void main(String[] args) {
SimpleBloomFilter filter = new SimpleBloomFilter();
filter.add("user:1001");
filter.add("user:1002");
filter.add("product:5001");
System.out.println(filter.mightContain("user:1001")); // true
System.out.println(filter.mightContain("user:9999")); // false(大概率)
System.out.println(filter.mightContain("product:5001")); // true
System.out.println(filter.mightContain("product:9999")); // false
}
}
生产建议:实际项目中推荐使用 Google Guava 的
BloomFilter 或 Redis 4.0+ 的布隆过滤器模块(bf.add、bf.exists),避免重复造轮子。
2.5 布隆过滤器在缓存穿透防护中的部署架构
最佳实践:布隆过滤器 + 空值缓存组合使用。布隆过滤器拦截大部分非法 Key,空值缓存兜底少量合法但数据不存在的场景,数据库压力可降低 99% 以上。