真实场景:防止缓存穿透 —— 使用 Redisson 布隆过滤器
🌐 真实场景:防止缓存穿透 —— 使用 Redisson 布隆过滤器
一、问题背景:什么是缓存穿透?
缓存穿透 是指:
- 用户(或恶意请求)频繁查询一个数据库中根本不存在的数据,比如商品 ID 为
-1、9999999这种根本不存在的数据。 - 由于缓存中没有这个 key,请求会直接打到数据库。
- 如果这样的请求量非常大,数据库就会承受大量无效查询,可能导致 数据库压力过大甚至宕机。
二、传统解决办法有哪些不足?
常见的解决办法有:
-
缓存空对象: 即使数据库查不到,也把空结果(比如 null)缓存起来,并设置较短的过期时间。
- ✅ 能挡一部分,但依然有无效查询,且对于大量不存在的 key,缓存也无效。
- ❌ 对于恶意构造的大量不存在 key 仍然无效,比如攻击者不断请求随机 ID。
-
接口限流、黑白名单:
- 可以挡一部分恶意流量,但无法从根本上解决大量正常业务中存在的无效查询问题。
三、更好的方案:布隆过滤器 + 缓存
我们可以在缓存层之前加一道 布隆过滤器 的屏障,它的作用是:
在查询缓存或数据库之前,先判断这个请求的 ID(比如商品ID、用户ID)是否可能存在于我们的系统中。如果布隆过滤器告诉我们 “这个 ID 一定不存在”,那就直接返回,不去查缓存,也不查数据库!
✅ 具体真实案例:电商系统商品查询防穿透
场景描述:
假设你有一个 电商系统,用户可以通过商品 ID 查询商品详情,比如:
GET /product/{productId}
- 正常的商品 ID 是 1、2、3… 100000(存储在数据库中)。
- 但是有一些用户(或爬虫/攻击者)会尝试访问一些 根本不存在的商品 ID,比如 99999999、-1、随机字符串数字等。
- 这些请求会穿过缓存,直接打到数据库,影响性能。
解决方案:使用 Redisson 布隆过滤器进行前置拦截
步骤 1:系统启动时,将所有合法的商品 ID 加入布隆过滤器
比如系统中有 10 万件商品,商品 ID 是从 1 到 100000(或者是数据库中的真实 ID)。
你可以在 系统启动时(比如 Spring Boot 的 CommandLineRunner 或 @PostConstruct),将所有有效的商品 ID 加入布隆过滤器:
// 假设你从数据库中查询出了所有有效的商品 ID 列表
List<Long> validProductIds = productService.getAllValidProductIds(); // [1, 2, 3, ..., 100000]// 初始化 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);// 获取布隆过滤器,命名为 "productBloomFilter"
RBloomFilter<Long> productBloomFilter = redisson.getBloomFilter("productBloomFilter");// 预期插入 10 万条,可接受误判率 0.01(1%)
productBloomFilter.tryInit(100000L, 0.01);// 将所有合法的商品 ID 加入布隆过滤器
for (Long productId : validProductIds) {productBloomFilter.add(productId);
}
⚠️ 注意:你也可以不一次性加载所有 ID,而是随着商品创建/导入时,实时往布隆过滤器里添加。但务必保证 所有有效 ID 都加入到了布隆过滤器中。
步骤 2:在查询商品接口处,先经过布隆过滤器拦截
当用户请求 /product/{productId} 时,后端逻辑如下:
@GetMapping("/product/{productId}")
public ResponseEntity<Product> getProduct(@PathVariable Long productId) {RBloomFilter<Long> productBloomFilter = redissonClient.getBloomFilter("productBloomFilter");// 第一步:先判断该商品 ID 是否可能存在于布隆过滤器中if (!productBloomFilter.contains(productId)) {// 布隆过滤器说:“这个商品 ID 一定不存在!”log.warn("请求了不存在的商品 ID: {}", productId);return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);}// 第二步:布隆过滤器说“可能存在”,继续查询缓存或数据库Product product = productService.getProductFromCacheOrDB(productId);if (product == null) {return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);}return ResponseEntity.ok(product);
}
四、这样做有什么好处?
| 优点 | 说明 |
|---|---|
| ✅ 阻挡大量无效请求 | 对于那些 根本不存在的商品 ID,布隆过滤器能够快速判断并拦截,避免查询缓存和数据库,极大减轻后端压力。 |
| ✅ 性能极高 | 布隆过滤器是基于 Redis 的位操作,判断是否存在的时间复杂度是 O(1),非常快。 |
| ✅ 分布式支持 | 多个服务实例共享同一个 Redis 中的布隆过滤器,所有服务都能统一判断 ID 是否合法,避免各自为政。 |
| ✅ 内存占用小 | 相比缓存所有不存在的 key,布隆过滤器用极小的内存开销,就能管理上百万级别的数据存在性。 |
| ✅ 防恶意攻击 | 对于爬虫、恶意用户发起的大量随机 ID 请求,可以有效防御。 |
五、布隆过滤器的误判怎么办?
布隆过滤器是 概率型 的,它可能出现 误判(false positive),即:
布隆过滤器说 “这个商品 ID 可能存在”,但实际上它不存在。
但请注意:
- 它绝不会漏判(false negative):如果布隆过滤器说 “不存在”,那这个 ID 100% 不存在。
- 所以我们的策略是:
- 如果布隆过滤器判断 “不存在” → 直接拦截,不查缓存/DB
- 如果布隆过滤器判断 “可能存在” → 继续走正常流程:查缓存 → 查 DB
即使有 少量误判(比如误认为 1000 个不存在的 ID 可能存在),也只是会导致 多查一次缓存/DB,但不会对系统造成大的影响,而且误判率可以控制得非常低(比如 1% 甚至更低)。
🧠 总结:这个真实例子教会我们什么?
| 场景 | 解决方案 | 工具/技术 |
|---|---|---|
| 大量用户请求不存在的数据(缓存穿透) | 在查询缓存/DB 前,先用布隆过滤器判断 ID 是否可能存在 | Redisson Bloom Filter + Redis |
| 目的 | 避免无效查询,保护数据库,提高系统稳定性 | 前置过滤器机制 |
| 优势 | 高性能、低内存、分布式一致、易集成 | 布隆过滤器 + Redis + Redisson |
