0015 布隆过滤器原理与应用

Posted on Wed, Sep 4, 2024 bloom filter JAVA 解决方案

1.什么是布隆过滤器?

布隆过滤器(Bloom Filter)是一种概率数据结构,用于判断输入数据是否在某个超大的数据集中,而且算法复杂度为常量时间O(K),其中K为哈希函数的个数。所谓概率数据结构是指它们针对大规模数据使用概率和统计的方法,以牺牲一定的准确性为代价来换取更高的性能和更少的内存消耗。

2.布隆过滤器原理

布隆过滤器的组成有以下几部分:

当输入值经过哈希函数映射,只要任意一个在位数组上不存在,说明输入值在数据集中一定不存在。反之如果输入值经过K个哈希函数映射后在位数组上都存在,只能说明输入值有很大概率在数据集中是存在的,但不能百分之百保证,这是因为哈希冲突始终存在的原因。也因此布隆过滤器被称为概率数据结构,它充当了输入值的过滤器,将一定不存在的输入值过滤掉,避免系无效输入。图6-4展示了位数组的查询过程。

图6-4 位数组读取

当我们的系统接受到写入操作时,会同时更新位数组。图6-5为位数组的写入过程,其中分别计算K个哈希函数的值并映射到数组,将该位置的值置为1,即存在。

图6-5 位数组写入

3.布隆过滤器的实现

我们借助JDK中的位集java.util.BitSet来实现一个简单的布隆过滤器。JDK中的BitSet 本质上是一个long数组,数组每个元素是long类型(bit数为64),假设long数组长度为n,则相当于构造了一个长度为64×n位的位数组。

图6-6 JDK中BitSet数据结构

图6-6为BitSet示意图,还记得我们之前介绍的二维数组和一维数组的关系吗?两者之间是可以转换的,因此我们可以用二维数组来表示一个超长的一维数组,这就是BitSet的核心思想。这里的关键在于给定一个哈希索引值i,如何确定它在BitSet中的位置。根据我们前面介绍过的转换关系不难得出,索引i在BitSet的long数组的第i/64个位置的第i%64个bit位。

以BitSet为基础,一个简单的布隆过滤器的实现如下。

import java.util.BitSet;
import java.util.Random;

public class BloomFilter {
    private final int size; // 过滤器bit长度
    private final BitSet bitSet;  // 位集
    private final int[] hashSeeds;  // 哈希种子数组
    private final int numHashFunctions;  // 哈希函数个数
    // 构造函数,初始化布隆过滤器
    public BloomFilter(int size, int numHashFunctions) {
        this.size = size;
        this.bitSet = new BitSet(size);
        this.numHashFunctions = numHashFunctions;
        this.hashSeeds = new int[numHashFunctions];
        initializeHashSeeds();
    }

    /**
     * 初始化哈希种子
     */
    private void initializeHashSeeds() {
        Random random = new Random();
        for (int i = 0; i < numHashFunctions; i++) {
            hashSeeds[i] = random.nextInt();
        }
    }

    /**
     * 添加元素到布隆过滤器
     * @param value 添加的字符串元素值
     */
    public void add(String value) {
        for (int i = 0; i < numHashFunctions; i++) {
            int hash = hash(value, hashSeeds[i]);
            bitSet.set(Math.abs(hash % size), true);
        }
    }

    /**
     * 检查元素是否存在于布隆过滤器
     * @param value 输入元素
     * @return 是否存在true/false
     */
    public boolean mightContain(String value) {
        for (int i = 0; i < numHashFunctions; i++) {
            int hash = hash(value, hashSeeds[i]);
            if (!bitSet.get(Math.abs(hash % size))) {
                return false;
            }
        }
        return true;
    }

    /**
     * 简单的哈希函数
     * @param value 元素值
     * @param seed 种子值
     * @return 哈希值
     */
    private int hash(String value, int seed) {
        int result = 0;
        for (int i = 0; i < value.length(); i++) {
            result = result * seed + value.charAt(i);
        }
        return result;
    }
}

我们通过测试类来验证这个过滤器,代码中我们构建了一个bit长度为1000的位数组,使用了5个哈希函数。并添加了两个元素在过滤器中,打印查找效果,且实验了元素未添加时的打印结果,结果可能为false(不存在),也可能为true(有哈希冲突但实际并不存在)。验证代码如下。

public class BloomFilterTest {
    public static void main(String[] args) {
        // 创建一个布隆过滤器,大小为1000,使用5个哈希函数
        BloomFilter bloomFilter = new BloomFilter(1000, 5);

        // 添加一些元素
        bloomFilter.add("hello");
        bloomFilter.add("world");

        // 检查元素是否存在
        System.out.println(bloomFilter.mightContain("hello")); // 输出:true
        System.out.println(bloomFilter.mightContain("world")); // 输出:true
        System.out.println(bloomFilter.mightContain("OK")); // 输出:false (可能)
    }
}

4.布隆过滤器的应用场景

4.1 弱密码校验

你可能遇到过这样的情景:你注册成为某个网站的用户,当你设置的密码过于简单时(如:abcd1234),系统会提示弱密码并要求你重新设置。这里的弱密码校验就用到了布隆过滤器。首先后台会将庞大的弱密码落库,也可能是请求的第三方,但无论如何总有一个环节需要存储这些弱密码,数据量可能是千万级甚至上亿。如果我们直接使用用户的密码(通常都是加密过的,不允许明文传输和存储)去数据库中匹配,这个查询量是非常耗时的,因此需要使用布隆过滤器。

图6-7 布隆过滤器在弱密码校验中的应用

图6-7中,后台维护了一个弱密码库,通常都是明文的,如果是请求后台判断弱密码,那么需要采用对称加密算法将明文密码加密后,写入到布隆过滤器。用户设置了密码后,经过同一套加密算法加密传到后台,查询布隆过滤器,如果命中,说明可能是弱密码,如果有一个哈希函数未命中则一定不是弱密码。 在安全级别很高的系统,如银行或金融系统中,密码通常要求使用加盐加密,即在密码的前面或后面加上一串随机因子(字符串),然后再加密传输。随机因子的引入,使得即使相同的密码,它们加密后的值也不一样,这就导致弱密码库生成的布隆过滤器无法使用。因此,另一种思路是将布隆过滤器放到前端,在前端完成判断,无需将密码传到后台,如图6-8所示。

图6-8 布隆过滤器放置于前端

将布隆过滤器放置在前端需要注意的事项:

4.2 判断注册账号是否被占用

另一个场景是判断用户注册账号是否被占用。以社交软件为例,当用户使用邮箱或手机号注册新用户时,系统会快速判断这个账号是否已经存在,如果存在则不允许注册为新用户。假设平台的用户量很大,比如上亿,此时如果直接去后台查询数据库判断,其代价是非常大的。采用布隆过滤器就能快速判断一定不存在的账户,允许用户注册。如果通过布隆过滤器发现账户已存在,则再将其传入后台查询,进行二次精准确认。这种方式能将绝大部分查重请求在布隆过滤器这一层完成,保护了后端系统。

4.3 判断恶意网址

以谷歌浏览器Chrome为例,当用户输入一个URL,Chrome 都会首先使用浏览器中的本地布隆过滤器进行检查。此过滤器中填充了经过哈希处理的恶意 URL列表。如果布隆过滤器返回是,Chrome会请求Google的服务器进一步验证。如果布隆过滤器返回不是,Chrome就不会进行服务器检查,从而节省时间和服务器资源。

4.4 大数据系统数据检索

Apache Cassandra和HBase等大数据系统使用布隆过滤器进行快速数据检索。这些数据库不再需要每次检查特定数据是否存在时都对磁盘读取,而是使用布隆过滤器进行快速的首次检查。如果布隆过滤器返回存在,系统可能会执行磁盘读取以确认。但如果返回不存在,系统就会避免磁盘读取,从而节省大量时间。

4.5 解决缓存击穿

缓存击穿是指查询请求在缓存数据库(如Redis)和持久数据库(如MySQL)中都查不到指定数据,这会让系统做“无用功”。当大量不存在的请求冲击时,很可能将数据库打垮。此时布隆过滤器就派上了用场,将数据库中的数据映射到过滤器上,请求进来时先经过过滤器完成第一道判断,如果结果是可能存在,才允许请求数据库二次查询确认。