布隆过滤器(Bloom Filter)是一个在数据科学领域广泛应用的概念,即使你没有使用过,也一定听说过。它的主要功能是解决海量数据的存在性检测问题,尤其在判断某个元素是否存在于庞大数据集中时尤为重要,且能够容忍一定的误差。这一特性使得布隆过滤器在防止缓存穿透与海量数据去重等场景中表现出色。

文章内容概览

  1. 布隆过滤器的定义
  2. 布隆过滤器的工作原理
  3. 布隆过滤器的应用场景
  4. 手动在Java中实现布隆过滤器
  5. 使用Google开源的Guava中的布隆过滤器
  6. Redis中的布隆过滤器

什么是布隆过滤器?

布隆过滤器(Bloom Filter,BF)由1970年提出的计算机科学家Bloom所创造。它是由一个二进制向量(或位数组)和一组随机映射函数(哈希函数)组成的数据结构。与传统的数据结构如List、Map、Set相比,布隆过滤器在空间占用和运行效率上具有显著优势。不过,它的缺陷在于返回的结果是概率性的,可能会出现误判。此外,存放在布隆过滤器中的数据难以删除。

布隆过滤器利用一个较大的位数组来存储所有数据,数组的每个元素仅占用1 bit,且值为0(表示不存在)或1(表示存在)。例如,若要存放100万个元素,位数组仅需占用约122KB的存储空间。

图片

总结:

布隆过滤器是一种高效且性能优良的数据结构,用于检测元素在大集合中的存在性,但其存在一定的误判率和删除难度。理论上,添加到集合中的元素越多,误判的可能性也会随之增加。

布隆过滤器的工作原理

当一个元素被添加到布隆过滤器时,执行以下步骤:

  1. 使用哈希函数对元素进行计算,生成多个哈希值。
  2. 根据这些哈希值,将位数组中对应的下标值设为1。

当判断某个元素是否存在于布隆过滤器时,执行以下步骤:

  1. 对元素进行哈希计算,得到一系列哈希值。
  2. 检查位数组中对应位置的值,如果所有相关位置都是1,则说明该元素存在;如果有任何一个位置不是1,则说明该元素不在布隆过滤器中。

字符串在存储时会生成多个哈希值,将位数组的相应下标设置为1。这种方法使得去重变得非常简单。当第二次存储相同字符串时,因先前对应位置已设为1,便可快速判断该值的存在性。

需要注意的是,不同字符串可能会哈希到相同的位置,这种情况下可调整位数组的大小或哈希函数。因此,布隆过滤器可以说出某个元素存在时,可能会误判;而若布隆过滤器说某个元素不在,则该元素一定不在。

布隆过滤器的应用场景

  1. 元素存在性判断:如判断某个数字是否存在于庞大数字集中、避免缓存穿透、过滤垃圾邮件(检查邮件地址是否在黑名单中)等。
  2. 去重:用于爬虫程序中去重已经爬取的URL,或是对大量QQ号/订单号进行去重。

布隆过滤器在处理海量数据的存在性问题方面表现出色,因此广泛应用于上述场景。

编码实战

通过Java手动实现布隆过滤器

熟悉布隆过滤器的原理后,我们可以尝试手动实现一个。实现时需要:

  1. 一个合适大小的位数组用于存储数据。
  2. 几个不同的哈希函数。
  3. 添加元素到位数组的方法。
  4. 判断元素是否存在于位数组的方法。

以下是一个简单的实现代码(经过改进以适应所有类型对象):

import java.util.BitSet;

public class MyBloomFilter {

    private static final int DEFAULT_SIZE = 2 << 24; 
    private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; 

    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[SEEDS.length];

    public MyBloomFilter() {
        for (int i = 0; i < SEEDS.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    public void add(Object value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    public boolean contains(Object value) {
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public static class SimpleHash {

        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(Object value) {
            int h;
            return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
        }
    }
}

测试示例:

String value1 = "https://javaguide.cn/";
String value2 = "https://github.com/Snailclimb";
MyBloomFilter filter = new MyBloomFilter();
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));
filter.add(value1);
filter.add(value2);
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));

输出结果:

false
false
true
true

利用Google开源的Guava中的布隆过滤器

手动实现的目的是为了帮助理解布隆过滤器的原理,而Guava提供的实现是较为权威的。在实际项目中,我们通常不需要从头实现布隆过滤器,而是直接使用Guava的版本。

首先,需在项目中引入Guava依赖:

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

实际使用示例如下:

// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(
    Funnels.integerFunnel(),
    1500,
    0.01
);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));

在上述示例中,当mightContain()方法返回true时,我们可以99%确定该元素在过滤器中;如果返回false,则可以100%确定该元素不存在于过滤器中。

尽管Guava提供的布隆过滤器实现相当优秀,但它存在一个重要缺陷,即只能用于单机环境,且扩展容量不易。因此,在分布式场景下,我们需要使用Redis中的布隆过滤器。

Redis中的布隆过滤器

介绍

自Redis v4.0起,Redis引入了模块(Modules)功能,允许使用外部模块扩展其功能。布隆过滤器便是其中的一种模块。详细信息可参考Redis官方对Redis模块的介绍:https://redis.io/modules。

Redis官方推荐使用RedisBloom作为布隆过滤器的模块,地址为:https://github.com/RedisBloom/RedisBloom。

使用Docker安装

体验Redis中的布隆过滤器非常简单,只需通过Docker即可实现。你可以在Google上搜索“docker redis bloomfilter”,通常第一条搜索结果即可找到相关信息,具体地址为:https://hub.docker.com/r/redislabs/rebloom/(详尽介绍)。

具体操作如下:

➜  ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest  
➜  ~ docker exec -it redis-redisbloom bash  
root@21396d02c252:/data# redis-cli  
127.0.0.1:6379>  

注意:当前rebloom镜像已被废弃,官方推荐使用redis-stack。

常用命令一览

注意:key:布隆过滤器的名称,item:添加的元素。

  1. BF.ADD:将元素添加到布隆过滤器中,若该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}
  2. BF.MADD:将多个元素添加到布隆过滤器中并创建尚不存在的过滤器,操作方式与BF.ADD相同。格式:BF.MADD {key} {item} [item ...]
  3. BF.EXISTS:判断元素是否存在于布隆过滤器中。格式:BF.EXISTS {key} {item}
  4. BF.MEXISTS:判断一个或多个元素是否在布隆过滤器中。格式:BF.MEXISTS {key} {item} [item ...]

BF.RESERVE命令介绍:

命令格式如下:

BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]

参数说明:

  1. key:布隆过滤器的名称
  2. error_rate:期望的误报率,必须介于0到1之间。例如,期望的误报率为0.1%(1000中1),则设为0.001。该数字越接近零,则每个项目的内存消耗越大,CPU使用率也会增加。
  3. capacity:过滤器的容量。当实际存储的元素个数超出此值时,性能将下降,降级程度与超出限制的程度有关。

可选参数:

  • expansion:若创建了新的子过滤器,其大小将是当前过滤器大小乘以expansion,默认扩展值为2。

实际使用示例

127.0.0.1:6379> BF.ADD myFilter java  
(integer) 1  
127.0.0.1:6379> BF.ADD myFilter javaguide  
(integer) 1  
127.0.0.1:6379> BF.EXISTS myFilter java  
(integer) 1  
127.0.0.1:6379> BF.EXISTS myFilter javaguide  
(integer) 1  
127.0.0.1:6379> BF.EXISTS myFilter github  
(integer) 0