布隆过滤器的应用-黑名单过滤

293 阅读4分钟

什么是布隆过滤器:

布隆过滤器是一种空间效率极高的概率型数据结构,由伯顿·霍尔布鲁克·布隆在1970年提出。它主要用于判断一个元素是否可能属于一个集合,而非确定地给出答案。布隆过滤器的核心特点是可以快速地插入元素和查询元素是否存在,尽管这种存在性查询可能会出现小概率的误报,但绝不会出现漏报,即如果布隆过滤器判断一个元素不在集合中,则该元素肯定不在集合中。

基本组成:

  • 一个很长的二进制向量:所有的位初始设置为0。
  • 一系列哈希函数(K个不同的哈希函数):当一个元素插入布隆过滤器时,会经过这些哈希函数,每个函数都会将元素映射到位数组上的一个索引,然后将这些索引对应的位设置为1。

工作原理:

  • 插入元素时,通过哈希函数将其映射到位数组的多个位置,将这些位置的位设置为1。
  • 查询元素时,同样使用这些哈希函数映射到位数组,并检查这些位置的位是否全为1。如果全是1,则认为该元素可能存在于集合中;如果有任何一位为0,则能确定该元素不在集合中。

优势:

  1. 空间效率高:相比于其他精确的数据结构(如哈希表、集合等),布隆过滤器在表示大型集合时所需的空间很小。这是因为它是基于一个固定长度的位数组来存储数据,而不是存储每个元素的具体值。
  2. 插入和查询速度快:由于布隆过滤器只需要对元素进行几次哈希计算并将结果所在的位数组位置置1,所以插入和查询的时间复杂度均为O(K),其中K为哈希函数的数量,且通常远小于数据量的大小。
  3. 适合大规模数据:对于超大规模数据集,布隆过滤器能够很好地解决空间和查询性能的问题,尤其在分布式系统中,可以用作预处理阶段的高效过滤器,减少对后端资源的无效请求。
  4. 安全性增强:由于布隆过滤器并不直接存储元素的原始数据,所以在某些场合,它可以作为一种安全的解决方案,对数据隐私保护有严格要求的因为它仅保存元素的存在性信息而非具体内容。

缺点:

  1. 误报:布隆过滤器最大的缺点就是存在误报的概率。随着越来越多的元素被加入过滤器,误报的可能性会逐渐增大。这意味着,布隆过滤器可能会告诉你一个元素可能在集合中,但实际上它并不在。然而,布隆过滤器永远不会报告一个实际上在集合中的元素不在其中。
  2. 不可删除:标准布隆过滤器不支持有效地删除已添加的元素。一旦一个元素的哈希值对应的位被置为1,就不可能恢复原状,因为没有记录哪些位是由哪个元素置位的。
  3. 后期调整困难:一旦布隆过滤器初始化之后,若需更改其误报率或适应更多的元素,通常无法动态调整,可能需要重新创建一个新的布隆过滤器。

使用:

基于Google Guava实现布隆过滤器

引入Guava依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

示例代码

//初始化一个误差0.001的过滤器
public static BloomFilter<String> initBloomFilter(){
    Funnel<CharSequence> funnel = Funnels.unencodedCharsFunnel();
    //预期插入元素的数量
    int expectedInsertions = 100000;
    //可接受的错误率
    double fpp = 0.001;
    return BloomFilter.create(funnel, expectedInsertions, fpp);
}

//初始化一个误差0.001的过滤器
public static void putData(BloomFilter<String> filter){
    //使用PersonInfoSource模拟100000个不重复的手机号码
    HashSet<String> mobiles = new HashSet<>();
    while (true){
        String m = PersonInfoSource.getInstance().randomChineseMobile();
        mobiles.add(m);
        if (mobiles.size() == 100000){
            break;
        }
    }
    //数据写入写入过滤器
    for (String m : mobiles) {
        filter.put(m);
    }
}

//校验字符串是否存在
public static boolean mightContain(BloomFilter<String> filter,String str){
    return filter.mightContain(str);
}

基于redisson实现布隆过滤器

引入redisson依赖

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

初始化redisson

@Bean
public RedissonClient singleRedisson(RedisProperties redisProperties) throws IOException {
    log.info("============初始化redisson客户端============");
    // 创建配置
    Config config = new Config();
    config.setCodec(StringCodec.INSTANCE);
    config.setTransportMode(TransportMode.NIO);

    String address = "redis://" + redisProperties.getHost() +":"+ redisProperties.getPort();
    SingleServerConfig singleServerConfig = config.useSingleServer()
            .setAddress(address)
            .setDatabase(redisProperties.getDatabase());
    if (StringUtils.isNotEmpty(redisProperties.getPassword())){
        singleServerConfig.setPassword(redisProperties.getPassword());
    }
    return Redisson.create(config);
}

示例代码

//创建布隆过滤器
public RBloomFilter<String> createBloomFilter(String name, long expectedInsertions, double falseProbability) {
    RBloomFilter<String> bloomFilter = client.getBloomFilter(name);
    bloomFilter.tryInit(expectedInsertions, falseProbability);
    return bloomFilter;
}

//数据写入过滤器
public boolean add(String name, String data) {
    RBloomFilter<String> bloomFilter = client.getBloomFilter(name);
    return bloomFilter.add(data);
}

//校验字符串是否存在
public boolean contains(String name, String str) {
    RBloomFilter<String> bloomFilter = client.getBloomFilter(name);
    return bloomFilter.contains(str);
}

//删除整个过滤器,释放内存
public void delete(String name) {
    RBloomFilter<String> bloomFilter = client.getBloomFilter(name);
    bloomFilter.delete();
}