布隆过滤器详解
1、是什么
布隆过滤器(Bloom Filter):一个很长的二进制位数组 + N 个哈希函数 ,用来快速判断:某个元素一定不存在 / 可能存在。
✅特点:
- 判断不存在:100% 准确
- 判断存在 :有误判概率(可能把不在的元素判定为存在)
- 不能删除元素(普通布隆),只能新增、查询
- 不存储原始数据,只存 hash 映射后的 bit 位,内存占用极小
2、工作原理
- 初始化:长度为 m 的 bit 数组,全部置 0
- 添加元素 元素经过
k个不同哈希函数,算出 k 个下标,把数组对应下标全部置为1。
同一个位置多次置 1,还是 1。
- 查询元素是否存在 对元素执行同样 k 个 hash,拿到 k 个下标:
- 只要有任意一位是 0 → 元素一定不存在
- 全部都是 1 → 元素可能存在(存在误判)
误判来源:不同元素 hash 刚好落在同一批 bit 位上。
❌不能删除:如果把某个 bit 置 0,会影响其他共用这个 bit 的元素。
如果需要删除:使用计数布隆过滤器,bit 数组改成计数器数组。
3、核心参数
m:bit 数组长度(越大,误判率越低,内存越大)n:预期存放元素数量k:哈希函数个数- 误判率 p:业务上可以接受的错误概率
经验公式: 最佳哈希函数数量
k = (m/n)*ln2m 越大、n 越小,误判率越低。
4、Java/Redis 实际使用场景
场景 1:缓存穿透(最经典面试)
大量请求查询数据库不存在的数据,直接打穿 Redis 打到 DB。
方案:把所有有效 key 放入布隆过滤器。 请求过来先过布隆:
- 返回不存在 → 直接返回,不查 redis、不查 DB
- 返回可能存在 → 再查缓存、数据库
注意:布隆过滤器有误判,不会 100% 拦截。
场景 2:黑名单、垃圾过滤
黑名单手机号、url,判断是否在黑名单。
场景 3:去重
比如爬虫,判断 url 是否已经爬过;大数据量去重。
❗不能做:精确判断存在、需要删除元素的业务。
5、Redis 布隆过滤器
Redis4.0 以后提供插件 RedisBloom
# 添加
BF.ADD bloom_user 10001
# 查询
BF.EXISTS bloom_user 10001
Redis 布隆也不支持删除。
实例 1:Redis 布隆过滤器解决缓存穿透(伪业务代码,Java)
前提:Redis 安装 RedisBloom 模块,命令:
BF.ADD、BF.EXISTS
业务:商品查询
- 项目启动预热:把数据库全部合法商品 id 加载进布隆过滤器
java
// 项目启动,把所有商品id存入布隆
List<Long> allGoodsId = goodsMapper.selectAllGoodsId();
for (Long id : allGoodsId) {
// BF.ADD bloom:goods 1001
redisTemplate.opsForValue().execute((RedisCallback<Object>) conn -> {
conn.bfAdd("bloom:goods", id.toString().getBytes());
return null;
});
}
- 查询商品完整流程
java
public Goods getGoodsById(Long goodsId){
//①第一步:布隆过滤器判断
Boolean mayExist = (Boolean)redisTemplate.opsForValue().execute(conn->{
return conn.bfExists("bloom:goods",goodsId.toString().getBytes());
});
//布隆返回false:一定不存在,直接返回,不走redis、不走DB
if(Boolean.FALSE.equals(mayExist)){
return null;
}
//②布隆说可能存在,查询Redis缓存
String cacheJson = redisTemplate.opsForValue().get("goods:"+goodsId);
if(cacheJson != null){
return JSONUtil.toBean(cacheJson,Goods.class);
}
//③redis未命中,查询数据库
Goods goods = goodsMapper.selectById(goodsId);
if(goods != null){
//写入redis缓存
redisTemplate.opsForValue().set("goods:"+goodsId,JSONUtil.toJsonStr(goods),30, TimeUnit.MINUTES);
}
return goods;
}
业务注意:新增商品,需要同步调用
BF.ADD把新 id 加入布隆; ⚠️删除商品,普通布隆无法删除,会产生脏数据。
实例2:Guava 布隆过滤器(Java 本地)
java
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
java
// 预期元素10万,误判率0.01
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),100000,0.01);
bloomFilter.put("zhangsan");
boolean exist = bloomFilter.mightContain("zhangsan");
Guava 布隆是本地内存,多实例环境无法共享,适合单机;分布式必须用 RedisBloom。
6、高频面试考点
- 为什么布隆过滤器不能删除?
一个 bit 位会被多个元素共享,把 bit 置 0,会把其他元素标记成不存在。 解决:计数布隆过滤器,每个位置存计数器,删除计数器减一。
- 为什么会误判?
哈希冲突,不同元素映射到同一组 bit。不会漏判,只会误判。 不存在一定是真的不存在;存在不一定真的存在。
- 缓存穿透使用布隆过滤器有什么缺点? 1)有一定误判率; 2)元素新增后布隆要同步; 3)数据大量删除,普通布隆无法处理; 4)Redis 布隆需要安装插件;Guava 本地实例之间数据不互通。
- 布隆过滤器、BitMap 区别
- BitMap:一个元素对应一个 bit,无误差,但是需要知道元素最大值,适合 id 连续场景。
- 布隆过滤器:k 个 hash 映射多个 bit,有误差,不需要知道元素范围,适合字符串、不规则 id。
7、对比小结
表格
| 特性 | 布隆过滤器 | BitMap |
|---|---|---|
| 误差 | 存在误判 | 无误差 |
| 存储原始值 | 不存储 | 不存储 |
| 删除 | 普通版本不支持 | 支持 |
| 适用数据 | 字符串、离散 id | 连续数字 id |
| 内存 | 极小 | 取决于最大值 |