目录
布隆过滤器介绍
布隆过滤器主要是为了解决海量数据的存在性问题。它是一种非常节省空间的概率数据结构,运行速度快,占用内存小。它实际上是一个很长的二进制向量和一系列随机映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。主要用于判断一个元素是否在一个集合中。主要是解决大规模数据下不需要精确过滤的场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等。
它最大的优点就是支持海量数据场景下高效率判断元素是否存在,而缺点是无法删除且容易出现【多个元素通过哈希后,可能会产生hash碰撞导致映射同一位置】,随着元素逐渐增多且容量不变,那么hash碰撞概率增大。
总体来说,它++判断某个元素存在,由于存在误判,这个元素不一定是存在的;而判断如果某个元素不存在,那这个元素一定不存在++。
结构分析
布隆过滤器主要使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,在初始状态,所有位置的位都是0,并且每个元素只能是 0 或者 1(代表 false 或者 true)。
当一个元素加入布隆过滤器中的时候,会进行如下操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
假设有两个hash函数,那么就会在两个位置赋值为1:

当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
使用布隆过滤器
首先引入依赖:
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
然后再 application.yml 写入配置:
bash
spring:
data:
redis:
database: 0
host: 127.0.0.1
port: 6379
password: ****
timeout: 3000
bloom-filter:
name: bloom-filter
expectedInsertions: 1000
falseProbability: 0.01
属性配置:
java
@Data
@ConfigurationProperties(prefix = BloomFilterProperties.PREFIX)
public class BloomFilterProperties {
public static final String PREFIX = "bloom-filter";
/**
* 布隆过滤器名字
*/
private String name;
/**
* 布隆过滤器的容量,初始化容纳约 2 万个 key
*/
private Long expectedInsertions = 20000L;
/**
* 布隆过滤器碰撞率
*/
private Double falseProbability = 0.01D;
}
然后自动装配:
java
@EnableConfigurationProperties(BloomFilterProperties.class)
public class BloomFilterAutoConfiguration {
/**
* 布隆过滤器
*/
@Bean
public BloomFilterHandler rBloomFilterUtil(RedissonClient redissonClient, BloomFilterProperties bloomFilterProperties) {
return new BloomFilterHandler(redissonClient, bloomFilterProperties);
}
}
然后写一个布隆过滤器工具组件:
java
/**
* 布隆过滤器处理器(用于解决缓存穿透问题)
*
* 作用:
* 1. 在访问 Redis / 数据库之前,先判断某个 key 是否"可能存在"
* 2. 对数据库中一定不存在的 key 进行拦截,避免缓存穿透
*
* 技术实现:
* - 基于 Redisson 提供的 RBloomFilter
* - 数据实际存储在 Redis 中
* - 支持分布式、多实例共享
*/
public class BloomFilterHandler {
/**
* 用于缓存穿透治理的布隆过滤器实例
*
* 特点:
* - 内部是位数组 + 多个 hash 函数
* - 判断结果只有两种:
* false:一定不存在(可直接拦截请求)
* true :可能存在(继续走缓存/数据库流程)
*/
private final RBloomFilter<String> cachePenetrationBloomFilter;
/**
* 构造方法:初始化布隆过滤器
*
* @param redissonClient Redisson 客户端,用于获取 Redis 中的 BloomFilter
* @param bloomFilterProperties 布隆过滤器配置参数(容量、误判率等)
*/
public BloomFilterHandler(RedissonClient redissonClient,
BloomFilterProperties bloomFilterProperties) {
// 根据配置的名称,从 Redis 中获取(或创建)一个布隆过滤器
// 多个服务实例拿到的是同一个 BloomFilter
RBloomFilter<String> cachePenetrationBloomFilter =
redissonClient.getBloomFilter(bloomFilterProperties.getName());
// 初始化布隆过滤器:
// expectedInsertions:预计插入的元素数量
// falseProbability :允许的误判率
// tryInit 语义:
// - 如果 BloomFilter 已存在,不会重复初始化
// - 避免服务重启导致布隆过滤器被重置
cachePenetrationBloomFilter.tryInit(
bloomFilterProperties.getExpectedInsertions(),
bloomFilterProperties.getFalseProbability()
);
// 赋值给成员变量,供后续 add / contains 使用
this.cachePenetrationBloomFilter = cachePenetrationBloomFilter;
}
/**
* 向布隆过滤器中添加一个元素
*
* 使用场景:
* - 系统启动时,将数据库中的主键批量写入 Bloom
* - 新增数据成功后,同步写入 Bloom
*
* @param data 需要加入布隆过滤器的 key(如 userId / orderNo)
* @return 是否添加成功
*/
public boolean add(String data) {
return cachePenetrationBloomFilter.add(data);
}
/**
* 判断某个 key 是否可能存在
*
* 缓存穿透的核心方法:
* - false:该 key 在数据库中一定不存在,可直接拦截请求
* - true :该 key 可能存在,需要继续查询缓存或数据库
*
* @param data 查询的 key
* @return 是否可能存在
*/
public boolean contains(String data) {
return cachePenetrationBloomFilter.contains(data);
}
/**
* 获取预计插入元素数量(配置值)
*
* 一般用于:
* - 运维监控
* - 压测评估
*/
public long getExpectedInsertions() {
return cachePenetrationBloomFilter.getExpectedInsertions();
}
/**
* 获取布隆过滤器的误判率
*
* 误判率越低:
* - 占用内存越大
* - 但被误放行的不存在 key 越少
*/
public double getFalseProbability() {
return cachePenetrationBloomFilter.getFalseProbability();
}
/**
* 获取布隆过滤器底层位数组大小
*
* 可用于评估内存占用情况
*/
public long getSize() {
return cachePenetrationBloomFilter.getSize();
}
/**
* 获取 hash 函数的个数
*
* hash 次数越多:
* - 判断越准确
* - 但性能开销略增
*/
public int getHashIterations() {
return cachePenetrationBloomFilter.getHashIterations();
}
/**
* 获取当前布隆过滤器中已插入的元素数量
*
* 注意:
* - 该值为近似值(非精确统计)
*/
public long count() {
return cachePenetrationBloomFilter.count();
}
}
我们以登录注册为例,可以在登录注册后添加它的手机号:
java
@Transactional(rollbackFor = Exception.class)
public Boolean register(UserRegisterDto userRegisterDto) {
// 业务逻辑...
bloomFilterHandler.add(userMobile.getMobile());
return true;
}
然后判断用户手机号在布隆过滤器中是否存在,如果判断存在的话,由于布隆过滤器有碰撞率,则需要在数据库中再次判断:
java
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 和数据库之前,提前判断请求 key 是否可能存在",从根源上拦截了数据库中一定不存在的数据请求,从而解决缓存穿透问题。
缓存穿透场景:
java
请求不存在的 key(如 userId=99999999)
↓
Redis miss
↓
MySQL 也不存在
↓
高并发 → DB 被打爆
由于系统不知道数据库不存在数据,所以只能查询数据库,那么我们可以将数据添加到布隆过滤器,在布隆过滤器中去判断是否拥有该数据,如果没有,则不需要继续查询:
java
if (!bloomFilterHandler.contains(key)) {
return null;
}
整体思路:
java
请求
↓
① BloomFilter 判断 key 是否存在
├─ 不存在 → 直接返回(拦截穿透)
└─ 可能存在
↓
② Redis 查询
├─ 命中数据 → 返回
├─ 命中空值 → 返回 null
└─ 未命中
↓
③ 数据库查询
├─ 不存在 → 写入空值缓存(短 TTL)
└─ 存在 → 回写 Redis
查询代码:
java
@Service
@RequiredArgsConstructor
public class UserService {
private final BloomFilterHandler bloomFilterHandler;
private final RedisTemplate<String, Object> redisTemplate;
private final UserMapper userMapper;
private static final String USER_CACHE_KEY_PREFIX = "user:";
private static final long CACHE_TTL = 1; // 正常数据缓存 1 小时
private static final long NULL_CACHE_TTL = 2; // 空值缓存 2 分钟
public User getUserById(String userId) {
// ==============================
// ① 布隆过滤器:拦截一定不存在的请求(解决缓存穿透的核心)
// ==============================
if (!bloomFilterHandler.contains(userId)) {
return null;
}
String redisKey = USER_CACHE_KEY_PREFIX + userId;
// ==============================
// ② 查询 Redis
// ==============================
Object cache = redisTemplate.opsForValue().get(redisKey);
// ②-1 命中空值缓存
if (cache instanceof NullValue) {
return null;
}
// ②-2 命中正常数据
if (cache instanceof User) {
return (User) cache;
}
// ==============================
// ③ 查询数据库
// ==============================
User user = userMapper.selectById(userId);
// ③-1 数据库不存在 → 写入空值缓存(防止 Bloom 误判)
if (user == null) {
redisTemplate.opsForValue()
.set(redisKey, NullValue.INSTANCE, NULL_CACHE_TTL, TimeUnit.MINUTES);
return null;
}
// ③-2 数据库存在 → 回写缓存
redisTemplate.opsForValue()
.set(redisKey, user, CACHE_TTL, TimeUnit.HOURS);
return user;
}
}