真实场景:防止缓存穿透 —— 使用 Redisson 布隆过滤器

🌐 真实场景:防止缓存穿透 ------ 使用 Redisson 布隆过滤器


一、问题背景:什么是缓存穿透?

缓存穿透 是指:

  • 用户(或恶意请求)频繁查询一个数据库中根本不存在的数据 ,比如商品 ID 为 -19999999 这种根本不存在的数据。
  • 由于缓存中没有这个 key,请求会直接打到数据库。
  • 如果这样的请求量非常大,数据库就会承受大量无效查询,可能导致 数据库压力过大甚至宕机

二、传统解决办法有哪些不足?

常见的解决办法有:

  1. 缓存空对象: 即使数据库查不到,也把空结果(比如 null)缓存起来,并设置较短的过期时间。

    • ✅ 能挡一部分,但依然有无效查询,且对于大量不存在的 key,缓存也无效。
    • ❌ 对于恶意构造的大量不存在 key 仍然无效,比如攻击者不断请求随机 ID。
  2. 接口限流、黑白名单:

    • 可以挡一部分恶意流量,但无法从根本上解决大量正常业务中存在的无效查询问题。

三、更好的方案:布隆过滤器 + 缓存

我们可以在缓存层之前加一道 布隆过滤器 的屏障,它的作用是:

在查询缓存或数据库之前,先判断这个请求的 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 加入布隆过滤器:

java 复制代码
// 假设你从数据库中查询出了所有有效的商品 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} 时,后端逻辑如下:

java 复制代码
@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
相关推荐
mmm.c2 小时前
mysql启动提示1067:进程意外终止
数据库·mysql
埃泽漫笔2 小时前
Redis单线程还是多线程?
数据库·redis·缓存
TDengine (老段)3 小时前
TDengine 产品组件 taosX
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
一叶飘零_sweeeet3 小时前
MySQL 锁详解
mysql·innodb
沐伊~3 小时前
mysql 安装
数据库·mysql
成为你的宁宁3 小时前
Ubuntu安装mysql5.7及常见错误问题
linux·mysql·ubuntu
TimberWill3 小时前
CONCAT函数使用中出现空指针异常问题分析
数据库
TDengine (老段)3 小时前
TDengine 字符串函数 CHAR_LENGTH 用户手册
大数据·数据库·时序数据库·tdengine·涛思数据
wind_one13 小时前
5.基础--SQL--DDL数据库操作
数据库·sql