欢迎光临
我们一直在努力

缓存穿透问题的解决思路

一、前言:你的数据库是否正在被“恶意请求”压垮?

你是否遇到过这些现象?

  • 系统突然 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)


八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

赞(0)
未经允许不得转载:171主机测评 » 缓存穿透问题的解决思路
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址