布隆过滤器详解

180 阅读6分钟

一、什么是布隆过滤器?

简单的说布隆过滤器是一个数据结构,以牺牲一定的准确率为代价,换取储存空间与查询速率。

        复杂的讲,布隆过滤器包含一个位数组与一组哈希函数,初始时所有位都设置为 0,每个哈希函数都能将输入元素映射到位数组的某个位置。

        插入一个元素时,

        首先将要添加的元素 x 分别通过 k 个哈希函数计算,得到 k 个哈希值:h1(x)h2(x), ..., hk(x)

        然后将这些哈希值对位数组长度 m 取模(% m),得到 k 个位数组索引:index1 = h1(x) % mindex2 = h2(x) % m, ..., indexk = hk(x) % m

        最后将位数组中这 k 个索引位置的值都设置为 1

        查找元素时:

        第一步将要查询的元素 y 分别通过相同的 k 个哈希函数计算,得到 k 个哈希值。

        第二步将这些哈希值对位数组长度 m 取模,得到 k 个位数组索引。

        最后检查位数组中这 k 个索引位置的值:

                如果其中任何一个位置的值是 0,那么元素 y 肯定不存在于集合中。

                如果这 k 个位置的值都是 1,那么元素 y 可能存在于集合中(存在误判的可能)。

        也就是说布隆过滤器查到元素不存在,那它一定不存在,查到元素存在,它不一定存在,这里就是会出现误判的地方。

        误判率是可以自己设置,有哈希函数的个数决定,下面有讲解。 ​

二、布隆过滤器关键参数与性能指标

  1. 位数组大小 (m): m 越大,能容纳的元素越多,假阳性率越低,但占用的空间也越大。

  2. 哈希函数个数 (k): k 的选择很重要。k 太小,容易发生冲突(不同元素映射到相同位置的概率高);k 太大,位数组会被迅速填满(每个元素设置更多位),也会导致假阳性率上升。存在一个最优的 k 值来最小化给定 m 和预期元素数量 n 下的假阳性率。

  3. 预期元素数量 (n): 设计布隆过滤器时,需要预估将要添加的元素总数 n

  4. 假阳性率 (p): 这是布隆过滤器最关键的指标。假阳性率 p 可以通过以下公式近似计算(在 mnk 确定的情况下):

    p ≈ (1 - e^(-k * n / m)) ^ k
    

    • 可以看到,p 随着 m 的增大而减小。
    • p 随着 n 的增大而增大(集合越大,位数组越饱和,冲突概率越高)。
    • p 与 k 的关系是先减小后增大,存在一个最优的 k 值使得 p 最小。最优 k 近似为 (m / n) * ln(2)

​ ​

三、如何使用布隆过滤器

        1.导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.21.3</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

 2.配置Redis

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379

 3. 创建布隆过滤器实例

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置
 */
@Configuration
public class RBloomFilterConfiguration {

 
    @Bean
    public RBloomFilter<String> cachePenetrationBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("cachePenetrationBloomFilter");
        cachePenetrationBloomFilter.tryInit(100000000L, 0.001);
        return cachePenetrationBloomFilter;
    }
}

核心方法tryInit设置容量与误判率。

4. 代码中如何使用布隆过滤器

@Service
public class AService {

    @Autowired
    private RBloomFilter<String> cachePenetrationBloomFilter;

    public void testBloomFilter() {
        // 新增数据到布隆过滤器中
    	cachePenetrationBloomFilter.add("xing02");
        // 判断元素是否存在布隆过滤器
        boolean hasUsername = cachePenetrationBloomFilter.contains("xing02");
    }
}

 四、常见问题解答

1.如何评估布隆过滤器的容量

上述已经介绍过布隆过滤器的关键参数,当我们配置它的参数时(tryInit方法),我们都会有疑问,该设置多少合适呢?

布隆过滤器的容量取决于系统数量需求。 这方面需要一定的经验,我们需要根据系统的用户数量以及未来一段时间的增长量来判断该设置多少容量。建议适当设置大一点,当元素数量越来越接近布隆过滤器容量时,出现误判的可能性会增大。但是也不能设置太大,会造成一定空间的浪费。

第二个是误判率参数,即相应的哈希函数的个数。误判率越低通常需要更多的哈希函数,查询性能降低的同时,也会导致位数组会被迅速填满,最后又会导致误判率升高。

所以布隆过滤器的容量配置取决于系统实际情况与工程师的经验。

不过我们可以查询不同配置对应占用的内存大小。

布隆过滤器内存占用查询网站Bloom Filter Calculator

​编辑

初始化一个 1 亿元素误判率在千分之一的布隆过滤器,大概占据内存 170M 左右。

2、如何克服缺点?

对于误判:

  • 理解其概率性质:布隆过滤器报告“可能存在”时,需要结合业务逻辑进行二次确认(例如,去数据库/缓存中再次查找)。
  • 接受一定的误判率:如果应用场景可以容忍一定比例的误判(如一些缓存场景、推荐去重),那么假阳性是可以接受的代价。

对于无法删除:

  • 计数布隆过滤器: 将位数组中的每个位扩展为一个小的计数器(如 4 bits)。添加元素时增加计数器,删除元素时减少计数器。只有当计数器减到 0 时才重置为 0。这解决了删除问题,但显著增加了空间开销(不再是 1 bit/位置),并且存在计数器溢出的风险。
  • 另外还可以使用布谷鸟过滤器。支持删除,性能通常优于计数布隆过滤器。

五、总结

        布隆过滤器是一种在空间效率和查询速度上表现卓越的概率数据结构,其核心价值在于以允许一定误判为代价,实现极低空间开销下的快速“不存在”判断。它特别适合应用于需要处理海量数据、对空间敏感、能够容忍假阳性或能通过后续步骤处理假阳性的场景(如缓存、数据库、爬虫、网络过滤、分布式系统)。理解其假阳性特性、无法删除的限制以及参数选择的重要性是正确使用布隆过滤器的关键。当需要支持删除操作时,可以考虑其变种如计数布隆过滤器或布谷鸟过滤器。