Bloom Filter (布隆过滤器) 介绍以及简单的Java实现

定义

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

从简单的定义可以看出,使用布隆过滤器目的是为了优化元素查找的性能,不过布隆过滤器提升的是得到 这个元素(key)的存在性的性能。

 

场景

从解决的问题出发去了解一个工具会更容易理解。

比如我们有一个网站应用,背后用普通的关系型数据库(例如MySQL)做存储,而且做了性能考虑在数据库之前加入了缓存(例如Redis),但是如果有请求高频率在短时间内疯狂访问一个在缓存内不存在,且在数据库也不存在的用户数据,每个请求都会执行缓存的访问和数据库的访问,造成大量无意义的缓存访问压力和数据库访问压力。

这时候如果我们在这些无意义的,甚至是恶意攻击的请求访问我们的缓存或者数据库之前,就做一次快速的存在性校验,让不存在的请求直接返回,就可以很大程度上保护缓存和数据库。

从其他的博客还了解到一些典型的场景,

例如垃圾邮件和垃圾短信的检测,被归入垃圾邮件的发件箱或者垃圾短信的来源号码的数量可能是上亿的,这时候一个遍历扫描,即使是优化的字符串匹配算法的消耗和性能在用户端应该都是不可接受的。

例如爬虫避开爬过的URL。

 

原理

既然要解决上面的问题,那么我们就需要做存在性检查。

如果给我们20亿条交易数据,而且会不断的增加,在每次访问缓存或者数据库之前做一个存在性检查,最直接想到的可能是类似hashtable这样的数据结构,去存储每个交易的key,每次有新的交易进来,就加入一条新的记录,容量不够就扩容,每次有查询进来,就做一次查找操作,这样的设计在数据越来越大的时候,占用的空间会越来越大,假设我们使用mysql的 varchar(36) 做uuid,每一条有36个字节(36byte),20亿条数据需要消耗的内存是:

即使用hash去压缩每一个key,这个容量一直增长的特点似乎也没有很好的办法去避免。

 

这时候布隆过滤器就上场了,那么布隆过滤器是怎么做的呢?

  • 准备一个bit数组
  • 准备k个hash函数,输入目标的key值和salt,输出一个int类型的hash值
  • 每次有新的key值进入的时候,用这k个hash函数分别加密后得到k个hash值
  • 对于第3步得到的每个hash值value,把bit数组中的第value个位赋值成1
  • 每次有查询来的时候,对查询的目标也做k次hash得到k个value,如果k个value在bit数组中的值都是1,那么这个key的存在性就是true,如果有其中一个value的bit数组值不是1,存在性即为false

从上面的步骤可以知道,空间是可控的,假设是一个21亿多位长度的bit数组,理论上只有不到0.3Gb,时间和计算量也是可控的,但是取决于hash函数的效率。

用一个简单的图形来解释,就是这样:

但是有一个很明显的问题,任何hash函数,面对可能输入的任意key,都是有一定概率出现冲突的,所以布隆过滤器也无法避免,当冲突出现的时候,可能出现这样的场景,key1的k个hash值分别是123,234,345,456,且已经被写入过滤器的bit数组,即

bit[123] = 1, bit[234] = 1, bit[345] = 1, bit[456] = 1;

key3(与key1)的k个hash值刚好都冲突了,但实际上key3并不存在,但是布隆过滤器仍然会返回true。

这个问题得出的结论是,布隆过滤器说存在的key,不一定真的存在

那么反过来,布隆过滤器说不存在的key是否一定不存在呢?答案是一定不存在

因为相同的hash函数输入相同的key,得出的hash值一定是相同的。

上面的结论换言之,布隆过滤器有误差,误差的数学公式可以参考这个知乎答案,它跟bit数组的大小,k的大小,元素的数量都有关,但是一个容量足够大的通用布隆过滤器,一般可以达到几千万个元素的误差率在万分之几的数量级,具体可以参考github上一个C++的实现

误差在这里是完全可以容忍的,本身布隆过滤器就是为了拦截大部分无效的访问,偶尔漏过去几条是完全没问题的。

 

Java实现

这里贴一段我完全按照自己的理解写的java实现,为了学习和理解,并非是通用的过滤器。

hardcode了k的值为9,每个salt是一个素数,用于计算字符串的hash,bit数组的容量为0x7fffffff - 2.

这里使用的hash函数也并非是高性能,如果在真实场景可以使用MurmurHash或者Fnv算法。

import org.apache.commons.lang3.StringUtils;

public class BloomFilter {

    private static int[] salts = {3, 5, 7, 11, 13, 17, 19, 23, 29};

    // avoid VM error
    private static boolean[] bitMap = new boolean[Integer.MAX_VALUE - 2];

    // refer to String.hashCode function
    // the salt/seed in JDK String class is 31, all the prime salt values less then 31 could only generate Integer values
    public static int myHash(String s, int salt) {
        if (StringUtils.isEmpty(s))
            throw new RuntimeException("empty string");
        int h = 0;
        for (int i = 0; i < s.length(); i++)
            h = salt * h + s.charAt(i);
        // to prevent negative values
        return Integer.MAX_VALUE & h;
    }

    public BloomFilter() {
        for (int i = 0; i<Integer.MAX_VALUE - 2; i++)
            bitMap[i] = false;
    }

    public boolean contains(String target) {
        if (StringUtils.isEmpty(target))
            return false;
        for (int salt : salts)
            if(!bitMap[myHash(target, salt)])
                return false;
        return true;
    }

    public void put(String target) {
        if (StringUtils.isEmpty(target))
            throw new RuntimeException("empty string");
        for (int salt : salts)
            bitMap[myHash(target, salt)] = true;
    }

    public static void main(String[] args) {
        BloomFilter b = new BloomFilter();
        for (int i=0; i< 100000000; i++) {
            String s = "test" + i;
            b.put(s);
        }
        int count = 0;
        for (int i=0; i< 100000000; i++) {
            String s = "test" + i;
            if (b.contains(s))
                count++;
        }
        System.out.println(count);
        count = 0;
        for (int i=0; i< 1000000000; i++) {
            String s = "test" + i;
            if (b.contains(s))
                count++;
        }
        System.out.println(count);
    }

}

这里的main函数做了一次简单的测试,往过滤器里面提前写入100000000(一亿)个不一样的字符串,再去查询0到一亿编号和十亿编号的字符串,其中一亿之前的应该都是存在的,从一亿之后的字符串应该是全都不存在的。

打印的结果是:

100000000
100002204

Process finished with exit code 0

也就是说有2204个误差,不论跟一亿还是十亿的基数比,这很显然是可以接受的。

 

实用

其实现实中Redis本身就有布隆过滤器的插件,可以直接配置使用,并不需要自己去实现,更需要关心的可能是不同数据规模下如何进行调优,Redis的布隆过滤器是可以制定error_rate的,一般来说指定的error_rate越小,需要的空间和计算量都会越大,需要通过一些性能测试去选择我们需要且可以接受的参数。

详细的信息和使用可以参考开源的redis bloom repo

 

 

 

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值