文章目录
- 前言
- 什么是布隆过滤器
- 项目中引入布隆过滤器
- 与缓存结合的最佳实践
- 场景:高并发用户访问商品详情页(防止缓存穿透)
- 总结:
前言
okok 我们已经学完了 所有的redis中的常用的数据结构 下面就是进阶
我会用一系列的例子 去讲解 如何使用这些数据结构 以及 如何结合到我们的实际业务代码中去
本文将会介绍布隆过滤器 以及结合缓存的最佳实践
什么是布隆过滤器
Bloom过滤器是一种空间高效的概率性数据结构,用于快速判断一个元素是否可能存在于一个集合中。
基本组成:
位数组(Bit Array): 长度为m的二进制数组,初始全为0
哈希函数集合: k个独立的哈希函数 h₁, h₂, …, hₖ
元素集合: 需要存储的数据集合
工作流程分析:
插入操作 (Add)
- 对元素x计算k个哈希值: h₁(x), h₂(x), …, hₖ(x)
- 将位数组中对应位置设为1:
bit[h₁(x) % m] = 1
bit[h₂(x) % m] = 1
bit[hₖ(x) % m] = 1 …
查询操作 (Query)
- 对元素x计算k个哈希值: h₁(x), h₂(x), …, hₖ(x)
- 检查位数组中对应位置: if bit[h₁(x) % m] == 1 AND
bit[h₂(x) % m] == 1 AND
… AND
bit[hₖ(x) % m] == 1:
return “可能存在” else:
return “一定不存在”
怎么简单去理解
就是有 n个 Hash表 每次插入数据 首先hash(数据) 到这 N 个 hash表 每个位置设置为1
然后查询的时候
再次使用相同的hash函数去hash(数据) 到这 N 个 hash表 检查每个位置是否为1
若为1 则可能存在通过校验 (疑问 为什么是可能存在呢?)
若存在某一位为0 则一定不存在 (疑问 为什么是一定不存在呢?)
怎么理解:可能存在
因为存在hash冲突啊 还有可能另外的数据也把当前位置 修改成了1 所以是可能而不是一定
怎么理解:一定不存在
你想想 当前受检的hash位置 都是0 说明根本就没有插入过这个数据 如果插入过 则一定会把这一位 置为1 但是为0 说明这个元素没有插入过当前的布隆过滤器
项目中引入布隆过滤器
依赖:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.50.0</version></dependency>
配置类:
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {Config config = new Config();// 如果你是单机模式config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);}
}
@Service
@RequiredArgsConstructor
public class RedisBloomFilterService {private final RedissonClient redissonClient;public void initBloomFilter(String name, long expectedInsertions, double falseProbability) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);// 初始化:预估元素数量 + 期望误判率(如 0.01)bloomFilter.tryInit(expectedInsertions, falseProbability);}public void add(String name, String value) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);bloomFilter.add(value);}public boolean mightContain(String name, String value) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);return bloomFilter.contains(value);}
}
测试类
@SpringBootTest
class RedisBloomFilterServiceTest {@Autowiredprivate RedisBloomFilterService bloomFilterService;@Testvoid testBloomFilter() {String filterName = "user:bloom";// 初始化:10 万数据,误判率 1%bloomFilterService.initBloomFilter(filterName, 100_000, 0.01);bloomFilterService.add(filterName, "user_1001");bloomFilterService.add(filterName, "user_1002");assert bloomFilterService.mightContain(filterName, "user_1001"); // trueassert !bloomFilterService.mightContain(filterName, "user_9999"); // very likely false}
}
与缓存结合的最佳实践
三级防护体系
请求 → Bloom过滤器 → 本地缓存 → Redis缓存 → 数据库↓ ↓ ↓ ↓拦截99%无效请求 热点数据 分布式缓存 最终数据源
@Service
public class MultiLevelCacheService {private BloomFilter<String> bloomFilter; // L0: 布隆过滤器private Cache<String, Object> localCache; // L1: 本地缓存private RedisTemplate redisTemplate; // L2: Redis缓存private Database database; // L3: 数据库public Object getData(String key) {// L0: Bloom过滤器检查if (!bloomFilter.mightContain(key)) {return null; // 一定不存在}// L1: 本地缓存检查Object data = localCache.getIfPresent(key);if (data != null) {return data;}// L2: Redis缓存检查data = redisTemplate.opsForValue().get(key);if (data != null) {localCache.put(key, data); // 回填本地缓存return data;}// L3: 数据库查询data = database.findById(key);if (data != null) {redisTemplate.opsForValue().set(key, data, Duration.ofMinutes(30));localCache.put(key, data);} else {// 设置空值缓存,防止缓存穿透redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));}return data;}
}
场景:高并发用户访问商品详情页(防止缓存穿透)
用户通过 /product/{id} 请求商品详情;
正常流程:Redis 缓存命中 → 返回数据;
若 Redis 未命中,就查 DB → 然后写入 Redis;
问题:恶意请求 / 错误 id 请求不断打穿缓存,导致 DB 被打爆,这就是「缓存穿透」。
如何解决这个问题 需要使用布隆过滤器
- 用户请求商品 /product/99999
- 系统先检查 BloomFilter.contains(“99999”): → false → 直接拒绝,无需查缓存/数据库 → true → 正常查 Redis 缓存,未命中则查数据库
loomFilter 初始化(商品上线时构建)
@PostConstruct
public void initBloomFilter() {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:product");bloomFilter.tryInit(1_000_000L, 0.01); // 容量:100w,误判率:1%// 假设所有商品ID:1~1000000for (int i = 1; i <= 1000000; i++) {bloomFilter.add(String.valueOf(i));}
}
Controller 拦截请求:
@GetMapping("/product/{id}")
public ResponseEntity<?> getProduct(@PathVariable String id) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:product");if (!bloomFilter.contains(id)) {return ResponseEntity.status(HttpStatus.NOT_FOUND).body("非法商品ID,拒绝访问");}// 查询缓存String redisKey = "product:" + id;Object product = redisTemplate.opsForValue().get(redisKey);if (product != null) return ResponseEntity.ok(product);// 查询数据库(模拟)Product result = productService.queryById(id);if (result != null) {redisTemplate.opsForValue().set(redisKey, result, Duration.ofMinutes(30));return ResponseEntity.ok(result);} else {// 缓存空对象防止缓存穿透redisTemplate.opsForValue().set(redisKey, "NULL", Duration.ofMinutes(10));return ResponseEntity.status(HttpStatus.NOT_FOUND).body("商品不存在");}
}
测试代码
@Test
public void testBloomFilterConcurrency() throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(20);for (int i = 0; i < 1000; i++) {final int id = i;executor.submit(() -> {String fakeId = String.valueOf(id);boolean mayExist = redissonClient.getBloomFilter("bloom:product").contains(fakeId);if (mayExist) {System.out.println("合法ID:" + fakeId + " → 继续查缓存/DB");} else {System.out.println("非法ID:" + fakeId + " → 拦截");}});}executor.shutdown();executor.awaitTermination(1, TimeUnit.MINUTES);
}
总结:
布隆过滤器 快速判断一个元素是否可能存在于一个集合中