布隆过滤器你真的了解嘛?

528 阅读5分钟

布隆过滤器

1、布隆过滤器概念

相信大多数人听到布隆过滤器的时候都是在处理Redis的缓存穿透问题,那么缓存穿透是什么?布隆过滤器是什么?它又是如何解决问题的呢?

什么是布隆过滤器?

其实布隆过滤器(Bloom Filter) 早在上个世纪就被一位叫布隆的人提供,它的出现旨在解决检索问题,说白了布隆过滤器也就是为了检索一个元素是否在一个集合中

布隆过滤器的技术选型

既然知道了布隆过滤器是为了检索元素,那么如何判断一个集合中是否包含这个元素呢?

我相信大家都能明白:最直接的办法就是将所有集合中的元素保存起来,然后与目标元素一一进行比较。

image.png 保存数据的数据结构有很多,我们常见的数组、链表、树等等都是,可是当遇到数据量非常大的时候,存储的需求也会越来越大,检索的速度也随之慢了下来。

解决问题:

不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。

2、布隆过滤器优缺点

优点:

  • 因为存储的是二进制数据,因此占用的空间很小
  • 它的插入和查询速度是很是快的,时间复杂度是O(n)
  • 保密性很好,由于自己不存储任何原始数据,只有二进制数据

缺点:

  • 布隆过滤器并不是绝对的准确也有可能存在误差
  • 如果两个对象的hash值计算出一样,就会导致误差
  • 布隆过滤器删除数据并非如此简单,布隆过滤器无法保证删除的元素的确在布隆过滤器里面

3、布隆过滤器的应用

之前文章开头提到过,我们常常使用布隆过滤器来解决redis缓存穿透的问题,那么我们先看看什么是缓存穿透?

image.png

其实也就是说当我们以Redis作为缓存数据库的时候,如果查询的key不存在,则就会一直检索缓存,当在高并发场景下缓存中(包括本地缓存和Redis缓存)的某一个Key被高并发的访问没有命中,此时回去数据库中访问数据,导致数据库并发的执行大量查询操作,对DB造成巨大的压力

布隆过滤器插入数据 布隆过滤器就是一个二进制数据的集合。当一个数据加入这个集合时:

  • 经过K个哈希函数计算该数据,返回K个计算出的hash值
  • 这些K个hash值映射到对应的K个二进制的数组下标
  • 将K个下标对应的二进制数据改为1。 例如,第一个哈希函数返回a,第二个第三个哈希函数返回b与c,那么:a、b、c对应的二进制就会被改为1.

image.png

布隆过滤器的查询过程 布隆过滤器主要做用就是查询一个数据,在不在这个二进制的集合中,查询过程以下:

  • 经过K个哈希函数计算该数据,对应计算出的K个hash值
  • 经过hash值找到对应的二进制的数组下标
  • 判断:若是存在一处位置的二进制数据是0,那么该数据不存在。若是都是1,该数据存在集合中。

布隆过滤器存在的问题 刚才我们谈到缺点的时候说到,布隆过滤器其实查询是有误差,那么我们分析一下为什么?

  • 存在一种可能,数据库存储的是小狗,但是此时我们查询的key为dog,经过计算小狗dog的hash值一样
  • 那么可能查询dog 的时候,会产生误差

应用总结:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,数据库防止查询击穿,使用BloomFilter来减少不存在的行或列的磁盘查找。

4、手写一个布隆过滤器

通过上面是介绍,相信大家对布隆过滤器也有了自己的认识,那么我们可以尝试着自己模拟一下

public class MyBloomFilter {

    /**
     * 一个长度为10 亿的比特位
     */
    private static final int DEFAULT_SIZE = 256 << 22;

    /**
     * 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
     */
    private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};

    /**
     * 相当于构建 8 个不同的hash算法
     */
    private static HashFunction[] functions = new HashFunction[seeds.length];

    /**
     * 初始化布隆过滤器的 bitmap
     */
    private static BitSet bitset = new BitSet(DEFAULT_SIZE);

    /**
     * 添加数据
     *
     * @param value 需要加入的值
     */
    public static void add(String value) {
        if (value != null) {
            for (HashFunction f : functions) {
                //计算 hash 值并修改 bitmap 中相应位置为 true
                bitset.set(f.hash(value), true);
            }
        }
    }

    /**
     * 判断相应元素是否存在
     * @param value 需要判断的元素
     * @return 结果
     */
    public static boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (HashFunction f : functions) {
            ret = bitset.get(f.hash(value));
            //一个 hash 函数返回 false 则跳出循环
            if (!ret) {
                break;
            }
        }
        return ret;
    }

    /**
     * 测试。。。
     */
    public static void main(String[] args) {

        for (int i = 0; i < seeds.length; i++) {
            functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
        }

        // 添加1亿数据
        for (int i = 0; i < 100000000; i++) {
            add(String.valueOf(i));
        }
        String id = "123456789";
        add(id);

        System.out.println(contains(id));   // true
        System.out.println("" + contains("234567890"));  //false
    }
}

class HashFunction {

    private int size;
    private int seed;

    public HashFunction(int size, int seed) {
        this.size = size;
        this.seed = seed;
    }

    public int hash(String value) {
        int result = 0;
        int len = value.length();
        for (int i = 0; i < len; i++) {
            result = seed * result + value.charAt(i);
        }
        int r = (size - 1) & result;
        return (size - 1) & result;
    }
}