布隆过滤器+缓存穿透
完全解读布隆过滤器
一.简介
布隆过滤器(Bloom Filter)是由布隆于1970年提出的一种空间效率极高的概率型数据结构,可以快速判断一个元素是否属于某个集合。
它的核心优势在于:高效、低内存、支持大数据量场景,因此在反垃圾邮件、爬虫去重、缓存穿透防护等领域被广泛使用。
二.优缺点
✅ 优点
-
存储与查询时间复杂度为 O(k)(k为哈希函数数量)
-
节省空间,只存储 hash 映射结果,而不保存原始数据
-
支持大规模数据快速判断
❌ 缺点
-
存在误判:可能会误判“存在”,但不会误判“不存在”
-
无法删除元素:由于多个元素可能映射到同一个位置,删除会影响其他数据
-
误判率随数据量增加而上升
三.工作原理
-
数据结构:布隆过滤器底层是一个长度为
m
的二进制位数组(初始全为 0),配合k
个独立哈希函数。 -
添加元素流程:
-
对要添加的元素,使用多个哈希函数计算出多个下标
-
将这些下标位置的值置为 1
-
-
查询元素流程:
-
对查询元素使用相同哈希函数生成多个下标
-
如果所有下标位置值都为 1,则判断“可能存在”
-
如果有任意一位为 0,则判断“一定不存在“
-
-
流程图:
四.特点总结
-
判断如果某个元素存在,由于存在误判,这个元素不一定是存在的
-
判断如果某个元素不存在,那这个元素一定不存在
五.案例
1.添加依赖
-
如果是不依赖Redis而使用布隆过滤器:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
-
依赖Redis,使用Redisson中的布隆过滤器:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.4</version>
</dependency>
2. 结构配置
- 不依赖Redis而使用布隆过滤器:
@Configurationpublic class BloomFilterConfig {@Beanpublic BloomFilter<String> bloomFilter() {//100000:布隆过滤器容量0.01为误判率return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),100000, 0.01);}}
- 依赖Redis,使用Redisson中的布隆过滤器:
属性配置
@Data
@ConfigurationProperties(prefix = BloomFilterProperties.PREFIX)
public class BloomFilterProperties {/*** 配置文件前缀 bloom-filter*/public static final String PREFIX = "bloom-filter";/*** 布隆过滤器名字*/private String name;/*** 布隆过滤器的容量*/private Long expectedInsertions = 20000L;/*** 布隆过滤器碰撞率*/private Double falseProbability = 0.01D;
}
布隆过滤器操作
/*** 布隆过滤器操作工具类* 用于对 Redisson 提供的 RBloomFilter 进行封装,简化使用流程。*/
public class BloomFilterHandler {/*** 布隆过滤器实例,用于存储和判断数据是否存在*/private final RBloomFilter<String> cachePenetrationBloomFilter;/*** 构造函数,初始化布隆过滤器* * @param redissonClient Redisson 客户端,用于连接 Redis* @param bloomFilterProperties 布隆过滤器配置(名称、容量、误判率)*/public BloomFilterHandler(RedissonClient redissonClient, BloomFilterProperties bloomFilterProperties){// 获取指定名称的布隆过滤器实例RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter(bloomFilterProperties.getName());// 初始化布隆过滤器:预期元素数量 + 误判率(仅首次有效)
cachePenetrationBloomFilter.tryInit(bloomFilterProperties.getExpectedInsertions(), bloomFilterProperties.getFalseProbability());this.cachePenetrationBloomFilter = cachePenetrationBloomFilter;}/*** 添加元素到布隆过滤器* @param data 待添加的数据* @return 添加是否成功*/public boolean add(String data) {return cachePenetrationBloomFilter.add(data);}/*** 判断元素是否可能存在* @param data 要判断的数据* @return true 表示可能存在,false 表示一定不存在*/public boolean contains(String data) {return cachePenetrationBloomFilter.contains(data);}/*** 获取设置的预期插入数量*/public long getExpectedInsertions() {return cachePenetrationBloomFilter.getExpectedInsertions();}/*** 获取设置的误判率*/public double getFalseProbability() {return cachePenetrationBloomFilter.getFalseProbability();}/*** 获取布隆过滤器位数组的大小(bit 数)*/public long getSize() {return cachePenetrationBloomFilter.getSize();}/*** 获取布隆过滤器使用的哈希函数数量*/public int getHashIterations() {return cachePenetrationBloomFilter.getHashIterations();}/*** 获取布隆过滤器中当前元素数量(估算值)*/public long count() {return cachePenetrationBloomFilter.count();}
}
3.YML文件配置
Redis配置
spring:
data:
redis:
database: 0 # Redis 使用的逻辑数据库(0 ~ 15),默认使用第 0 个
host: 127.0.0.1 # Redis 服务器地址,本地为 127.0.0.1(本机)
port: 6379 # Redis 服务端口,默认是 6379
password: redis # Redis 登录密码
timeout: 3000 # 连接超时时间,单位为毫秒,3000ms = 3秒
布隆过滤器配置
bloom-filter:
name: user-register-bloom-filter # 布隆过滤器的逻辑名称(Redis中作为Key)
expectedInsertions: 1000 # 预计将插入的元素数量(n)
falseProbability: 0.01 # 误判率(p),这里是1%,可接受的“存在误判”概率
4.测试使用
判断用户手机号在布隆过滤器中是否存在。
如果判断存在的话,由于布隆过滤器有碰撞率,则需要在数据库中再次判断。
public void doExist(String mobile) {// 第一步:使用布隆过滤器判断手机号是否可能存在boolean contains = bloomFilterHandler.contains(mobile);if (contains) {// 第二步:布隆过滤器判断可能存在(有一定误判概率),再查数据库做二次确认LambdaQueryWrapper<UserMobile> queryWrapper = Wrappers.lambdaQuery(UserMobile.class).eq(UserMobile::getMobile, mobile); // 查询数据库UserMobile userMobile = userMobileMapper.selectOne(queryWrapper); // 第三步:如果数据库中确实存在该手机号,抛出异常if (Objects.nonNull(userMobile)) {throw new DaMaiFrameException(BaseCode.USER_EXIST); // 自定义异常,提示“用户已存在”}}// 如果布隆过滤器判断为“不存在”,则直接通过(一定不存在,无需查库)// 如果布隆过滤器判断为“存在”,但数据库不存在,则视为新手机号,可继续注册
}
如何解决缓存穿透
一.定义
缓存穿透是指查询的数据在缓存和数据库中都不存在,导致每次查询这条数据都会穿透过缓存,直接去查询数据库,相当于没有缓存一样。
二.危害
一般存在缓存是为了缓解数据库的压力,如果短时间内发生了大量的请求并缓存穿透,就会试数据库的压力猛增,数据库的抗压能力比Redis要差的多得多,完全不是一个级别,所以如果是高并发的缓存穿透,极有可能造成系统宕机。
三.解决方案
-
缓存空对象
当查询的数据在缓存中和数据库中都不存在时,就缓存一个空结果,比如null,并将这个空结果返回给前端,并设置一个过期时间,避免消耗太多的内存。
拿用户注册的逻辑来说:
-
用户1注册用户使用自己的手机号,查询缓存和数据库都不存在,接着在缓存中设置一个空值,过期时间30s
-
用户2注册用户使用自己的手机号,查询缓存和数据库都不存在,接着在缓存中设置一个空值,过期时间30s
存在的问题
当短时间内大量用户来注册,每个用户都是用自己的手机号,缓存空值没有得到复用,除非手机号重复了,但这也不可能。所以还是穿透了缓存,请求都落到了数据库上,所以这种方案适合缓存空值能复用的场景。对于用户注册业务来说,不太适合。
-
-
分布式锁
分布式锁是防止并发问题最常用的解决方案了,核心就是加一把锁,每次只有一个请求能获得到锁,没有获得锁的请求等待获得锁的请求执行完后释放锁,然后再次竞争。所以解决缓存穿透也是可以的。
存在的问题
所谓分布锁,就是让请求变得串行化。每次只有一个请求执行,其他请求只能等待,这样会降低项目的并发量,对于并大量不高的项目来说,这种方案是可以的,但是对于大麦网高并发的项目来说,短时间的大量用户请求需要一个一个的执行,非常的影响用户体验,所以这种方案也不是很适合。
-
布隆过滤器
采用布隆过滤器,虽然在判断对象存在的时候会存在误判,这是由于存在hash碰撞而产生的。但是判断对象不存在则是一定不存在的。
使用布隆过滤器,存在的对象再去数据库进行查询,这样就解决的误判的问题。而对于不存在的对象直接返回结果即可。