一、前言:你的数据库是否正在被“恶意请求”压垮?
你是否遇到过这些现象?
- 系统突然 CPU 飙升,数据库连接池被打满
- 监控显示大量 SELECT * FROM user WHERE id = -1 请求
- 缓存命中率暴跌至 10% 以下
根本原因:缓存穿透(Cache Penetration)攻击!
缓存穿透是高并发系统中最危险的缓存问题之一,轻则性能下降,重则服务雪崩。本文将带你彻底理解其原理,并掌握三种工业级解决方案。
二、什么是缓存穿透?
缓存穿透:指查询一个根本不存在的数据,由于缓存是“不命中时被动加载”,每次请求都会绕过缓存,直接打到数据库。
🌰 经典场景:
- 用户传入 id = -1 查询用户信息
- 黑客用脚本遍历 id=999999999 到 id=9999999999
- 数据库中无此记录 → 缓存不存储 → 每次都查 DB
正常流程:
请求 → 缓存(命中)→ 返回 ✅
缓存穿透:
请求(id=-1)→ 缓存(miss)→ 数据库(无结果)→ 不写缓存 → 下次仍查 DB ❌
⚠️ 危害:
- 数据库 QPS 暴增,CPU 100%
- 连接池耗尽,正常用户无法访问
- 可能引发缓存雪崩(DB 崩溃后所有请求失效)
三、解决方案一:缓存空值(Null Cache)—— 最简单有效
原理:
即使数据库返回 null,也将空结果缓存一段时间(如 1~5 分钟),避免重复查询。
代码示例(Java + Redis):
public User getUser(Long id) {
String key = "user:" + id;
// 1. 先查缓存
String json = redis.get(key);
if (json != null) {
return json.isEmpty() ? null : JSON.parseObject(json, User.class);
}
// 2. 查数据库
User user = userMapper.selectById(id);
// 3. 缓存结果(包括 null)
if (user != null) {
redis.setex(key, 3600, JSON.toJSONString(user));
} else {
// ⭐ 关键:缓存空值,防止穿透
redis.setex(key, 60, ""); // 60秒过期
}
return user;
}
✅ 优点:
- 实现简单,成本低
- 立即生效,防御效果好
⚠️ 注意事项:
- 空值 TTL 要短(建议 30~300 秒),避免长时间掩盖真实数据插入
- 内存占用:恶意请求可能生成大量空 key,需配合限流
四、解决方案二:布隆过滤器(Bloom Filter)—— 高性能拦截
原理:
在缓存前加一层布隆过滤器,快速判断“某个 key 是否可能存在”。
- 如果 BF 说“不存在” → 直接拒绝请求
- 如果 BF 说“可能存在” → 继续查缓存/DB
💡 布隆过滤器特点:
- 空间效率极高(1亿数据仅需几百 MB)
- 允许误判(False Positive),但绝不漏判(False Negative)
- 适用于只增不删或删除不敏感的场景(如用户 ID、商品 ID)
实现步骤:
1. 初始化布隆过滤器(启动时加载所有合法 ID)
// 使用 Guava BloomFilter
BloomFilter<Long> userIdFilter = BloomFilter.create(
Funnels.longFunnel(),
1_000_000, // 预期插入数量
0.01 // 误判率 1%
);
// 启动时加载所有用户 ID
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(userIdFilter::put);
2. 查询时先过 BF
public User getUser(Long id) {
// ⭐ 第一步:布隆过滤器拦截
if (!userIdFilter.mightContain(id)) {
throw new BusinessException("用户不存在");
}
// 后续走正常缓存逻辑…
}
✅ 优点:
- 几乎零性能损耗(一次哈希计算)
- 内存占用可控
- 适合 ID 类、枚举类数据
⚠️ 局限:
- 无法处理动态新增数据(除非实时同步 BF)
- 不适用于范围查询(如 name LIKE '%张%')
🔧 增强方案:结合 Redis 的 RedisBloom 模块(支持分布式 BF)
五、解决方案三:请求校验 + 限流 —— 安全兜底
即使有上述方案,仍需前置防护:
1. 参数合法性校验
public User getUser(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("无效ID");
}
// …
}
2. 接口限流(Sentinel / RateLimiter)
- 对 /user/{id} 接口做 QPS 限制(如 1000/s)
- 对单个 IP 限流(防刷)
3. 黑名单机制
- 记录频繁查询不存在 ID 的 IP,加入黑名单
- 使用 Redis 计数器:INCR ip:1.2.3.4:notfound
六、三大方案对比总结
| ✅ 缓存空值 | ⭐ | 中(取决于恶意请求量) | 所有场景 | 否(可能占内存) |
| ✅ 布隆过滤器 | ⭐⭐⭐ | 低(固定大小) | ID/枚举类数据 | 否(无法处理动态新增) |
| ✅ 校验+限流 | ⭐⭐ | 极低 | 所有接口 | 是(安全兜底) |
📌 生产环境推荐组合使用:
参数校验 + 布隆过滤器(ID类) + 空值缓存 + 接口限流
七、避坑指南:常见错误实践
❌ 错误 1:缓存 null 但永不过期
后果:真实用户注册后,永远查不到数据
正解:必须设置 TTL(如 60 秒)
❌ 错误 2:布隆过滤器容量预估过小
后果:误判率飙升,大量合法请求被拦截
正解:按 最大数据量 × 1.5 预留空间
❌ 错误 3:只依赖一种方案
风险:单一方案有盲区
正解:纵深防御(Defense in Depth)
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!



