布隆过滤器(Bloom Filter)

一、简述

Bloom Filter(布隆过滤器)是1970年由Burton Howard Bloom提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和不支持删除。

BloomFilter能解决什么问题?以少量的内存空间判断一个元素是否属于这个集合,代价是有一定的错误率

二、应用场景

布隆过滤器的应用场景围绕“海量数据快速去重/判断”展开,以下是典型场景及原理:

2.1 网络与缓存场景

  • URL去重(Web爬虫):爬虫需避免重复爬取同一URL,用布隆过滤器存储已爬URL,判断新URL是否已处理(数据量级可达数十亿,传统存储无法承受)。
  • HTTP缓存加速:缓存服务器(如Nginx)用布隆过滤器存储已缓存的URL,收到请求时先判断URL 是否在缓存中,命中则直接返回,避免回源拉取。

2.2 数据过滤场景

垃圾邮件过滤:邮件服务器用布隆过滤器存储“垃圾邮件发送者的 IP/域名黑名单”,收到邮件时快速判断发送方是否在黑名单中,减少后续校验成本。
数据库防穿透:在缓存(如Redis)与数据库之间加布隆过滤器,存储“数据库中已存在的主键”,请求查询不存在的主键时,直接返回空,避免穿透到数据库。

2.3 分布式系统场景

  • HBase随机读优化:HBase在列族(CF)级别配置布隆过滤器,存储“StoreFile中的行键/列键”,随机读(Get)时先通过布隆过滤器排除不包含目标键的StoreFile,减少IO操作。
  • P2P网络资源查找:P2P节点用布隆过滤器存储“本地拥有的资源索引”,其他节点查询资源时,先通过布隆过滤器判断该节点是否有目标资源,避免无效通信。

三、工作原理

Bloom Filter的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

Bloom Filter的一个例子集合Sxyz}。带有颜色的箭头表示元素经过kk3hash函数的到在Mbit数组)中的位置。元素W不在S集合中,因为元素W经过khash函数得到在Mbit数组)的k个位置中存在值为0的位置。

  1. 初始化一个数组,所有位标为0,A={x1,x2,x3,…,xm} (x1, x2,x3,…,xm初始为0)

  2. 将已知集合S中的每一个数组,按以下方式映射到A

    2.0 选取n个互相独立的hash函数h1,h2,…hk
    2.1 将元素通过以上hash函数得到一组索引值h1(xi),h2(xi),…,hk(xi)
    2.2 将集合A中的上述索引值标记为1(如果不同元素有重复,则重复覆盖为1,这是一个觅等操作)

  3. 对于一个元素x,将其根据2.0中选取的hash函数,进行hash,得到一组索引值h1(x),h2(x),…,hk(x)

    如果集合A中的这些索引位置上的值都是1,表示这个元素属于集合S,否则则不属于S

3.1 前提

  1. hash函数的计算不能性能太差,否则得不偿失
  2. 任意两个hash函数之间必须是独立的.
    即任意两个hash函数不存在单一相关性,否则hash到其中一个索引上的元素也必定会hash到另一个相关的索引上,这样多个hash没有意义

3.2 错误率

工作原理的第3步得出来的结论,一个是绝对靠谱的,一个是不能100%靠谱的。在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。关于具体的错误率,这和最优的哈希函数个数以及位数组的大小有关,而这是可以估算求得一个最优解的:哈希函数个数k、位数组大小m及字符串数量n之间存在相互关系。相关文献证明了对于给定的mn,当\(k = ln(2)* m/n\)时出错的概率是最小的。

具体的请看:Bloom Filter概念和原理

3.3 基本特征

从以上对基本原理和数学基础的分析,我们可以得到Bloom filter的如下基本特征,用于指导实际应用。

  1. 存在一定错误率,发生在正向判断上(存在性),反向判断不会发生错误(不存在性);
  2. 错误率是可控制的,通过改变位数组大小、hash函数个数或更低碰撞率的hash函数来调节;
  3. 保持较低的错误率,位数组空位至少保持在一半以上;
  4. 给定mn,可以确定最优hash个数,即k = ln2 * (m/n),此时错误率最小;
  5. 给定允许的错误率E,可以确定合适的位数组大小,即m >= log2(e) * (n * log2(1/E)),继而确定hash函数个数k
  6. 正向错误率无法完全消除,即使不对位数组大小和hash函数个数进行限制,即无法实现零错误率;
  7. 空间效率高,仅保存“存在状态”,但无法存储完整信息,需要其他数据结构辅助存储;
  8. 不支持元素删除操作,因为不能保证删除的安全性。

四、实现

4.1 手动实现

知道了布隆过滤器的原理之后就可以自己手动实现一个,步骤如下

  1. 一个合适大小的位数组保存数据
  2. 几个不同的哈希函数
  3. 添加元素到位数组(布隆过滤器)的方法实现
  4. 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
import java.util.BitSet;

public class MyBloomFilter {
    //位数组的大小
    private static final int DEFAULT_SIZE = 2 << 24;

    //通过这个数组可以创建 6 个不同的哈希函数
    private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};

    //位数组。数组中的元素只能是 0 或者 1
    private BitSet bits = new BitSet(DEFAULT_SIZE);

    //存放包含 hash 函数的类的数组
    private SimpleHash[] func = new SimpleHash[SEEDS.length];

    //初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
    public MyBloomFilter() {
        // 初始化多个不同的 Hash 函数
        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;
    }

    //静态内部类。用于 hash 操作!
    public static class SimpleHash {
        private int cap;
        private int seed;

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

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

4.2 guava的实现

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class GuavaBloomFilter {
    public static void main(String[] args) {
        //创建布隆过滤器,设置存储的数据类型,预期数据量,误判率(必须大于0,小于1)
        BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100, 0.01);
        //存入元素
        bloomFilter.put("test");
        //判断元素是否存在
        System.out.println(bloomFilter.mightContain("test"));
    }
}

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

Guava提供的布隆过滤器的实现还是很不错的,但是有一个重大的缺陷就是只能单机使用(容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到Redis中的布隆过滤器了。

4.3 redis的实现

4.3.1 介绍

Redis v4.0之后有了Module(模块/插件)功能,Redis ModulesRedis可以使用外部模块扩展其功能。布隆过滤器就是其中的Module。详情可以查看Redis官方对Redis Modules的介绍:https://redis.io/modules

另外,官网推荐了一个RedisBloom作为Redis布隆过滤器的Module地址:https://github.com/RedisBloom/RedisBloom其他还有:

RedisBloom提供了多种语言的客户端支持,包括:PythonJavaJavaScriptPHP

4.3.2 使用Docker安装

具体地址: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> Copy to clipboardErrorCopied

4.3.3 常用命令一览

注意: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),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。
  3. capacity:过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。

可选参数:

  • expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以expansion。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。

4.3.4 实际使用

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

参考文章

posted @ 2022-04-22 17:45  夏尔_717  阅读(943)  评论(0)    收藏  举报