布隆过滤器(Bloom Filter)

背景

我们以网络爬虫为例。网络间的链接错综复杂,爬虫程序在网络间“爬行”很可能会形成“环”。为了避免形成“环”,程序需要知道已经访问过网站的URL。当程序又遇到一个网站,根据它的URL,怎么判断是否已经访问过呢?

 第一个想法就是将已有URL放置在HashSet中,然后利用HashSet的特性进行判断。它只花费O(1)的时间。但是,该方法消耗的内存空间很大,就算只有1亿个URL,每个URL只算50个字符,就需要大约5GB内存。

 如何减少内存占用呢?URL可能太长,我们使用MD5等单向哈希处理后再存到HashSet中吧,处理后的字段只有128Bit,这样可以节省大量的空间。我们的网络爬虫程序又可以继续执行了。

 但是好景不长,网络世界浩瀚如海,URL的数量急速增加,以128bit的大小进行存储也要占据大量的内存。

 这种情况下,我们还可以使用BitSet,使用哈希函数将URL处理为1bit,存储在BitSet中。但是,哈希函数发生冲突的概率比较高,若要降低冲突概率到1%,就要将BitSet的长度设置为URL个数的100倍。

 但是冲突无法避免,这就带来了误判。理想中的算法总是又准确又快捷,但是现实中往往是“一地鸡毛”。我们真的需要100%的正确率吗?如果需要,时间和空间的开销无法避免;如果能够忍受低概率的错误,就有极大地降低时间和空间的开销的方法。 


 

布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率。主要应用场景:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,查询加速(比如基于key-value的存储系统)等。

  • 初始状态时,Bloom Filter是一个包含m位的位数组(bit-array),初始时候每一位都为0:

 

 

 

  • Bloom Filter使用k个相互独立的哈希函数,通过哈希函数将每个元素映射到{1,…,m}的范围中。比如当来了一个元素 a,进行判断,使用2个哈希函数,计算出该元素对应的Hash值为1和5,然后到Bloom Filter中判断第1位和第5位的值,上面全部为0,则a不在Bloom Filter内,将 a 添加进去,也就是将第1位和第5位的值从0修改为1。(布隆过滤器不需要存储元素本身)

bloomfilter

  • 之后再来的元素,要判断是不是在Bloom Filter内,也是同a一样的方法,通过K个hash函数计算对应的hash值,然后去相应的位上查看是否都是1,只有都是 1 才认为这个元素在集合内,如果有一个为0则不存在,都为1则极为可能存在:

bloomfilter

  • 随着元素的插入,Bloom filter 中修改的值变多,出现误判的几率也随之变大,当新来一个元素时,满足其在Bloom Filter内的条件,即所有对应位都是 1 ,这样就可能有两种情况,一是这个元素就在集合内,没有发生误判;还有一种情况就是发生误判,出现了哈希碰撞,这个元素本不在集合内。

bloomfilter

使用BloomFilter,有三个重要的值,错误率(false positive rate)、哈希函数个数以及BloomFilter位数组的大小,关于这三个值的最优配置有一个原则,(BloomFilter位数组大小)/(实际的元素个数)越大,错误率越低,但消耗的空间会越多。

 

实现

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

我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。

如果你想要手动实现一个的话,你需要:

  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)));
        }

    }
}

测试:

 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));

Output:

false
false
true
true

测试:

Integer value1 = 13423;
        Integer value2 = 22131;
        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));

Output:

false
false
true
true

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

在Google Guava library中Google提供了一个布隆过滤器的实现:com.google.common.hash.BloomFilter

https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java

 

布隆过滤器在大数据下的应用

比如hbase通过rowkey查询时候,使用布隆过滤器快速判断该rowkey是否存在于当前storefile,再比如hive orc结合布隆过滤器,判断某个stripe下是否存在要查询的字段

 

参考:
https://juejin.im/post/5cc5aa7ce51d456e431adac5
http://lxw1234.com/archives/2015/12/580.htm

https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md

posted @ 2020-01-07 14:36  sw_kong  阅读(656)  评论(0编辑  收藏  举报