布隆过滤器(Bloom Filter)

375 阅读4分钟

1、什么是布隆过滤器?


一种高效的匹配算法,用于判断某值是否存在。

结果只有两种:不存在、可能存在

2、实现原理

预先准备k个hash函数、开辟一个足够的bit数组(简单通俗的讲:就是一个很长很长的数组,每个元素的值都为0或1,hash函数计算后将对应下标的value置为1)

对需要处理的值进行每个hash函数的计算,得到k个结果,每个结果对应bit数组中的一个bit位,分别将对应的bit位结果置1。

3、为什么会有误差?

随着插入的值越来越多,被置为1的bit位就越多,这是可能会存在一个值A,进行k个hash函数得到的结果在bit数组对应位上均为1,所以可能会出现误判。

那么问题来了,hash函数个数为k,需要插入的元素个数为n,布隆过滤器需要开辟的长度为m,错误率为p,如何合理的计算呢?给出如下公式:

m = - (n * lnp) / ((ln2)^2)
k = m * ln2 / n

推导过程: 过程省略 (小学毕业的我流下了悔恨的泪水)
本例中k=4;求得m=5.77n;求得p约为 6.3% 即 1000个数据中约有63个误差判断
如需要提高准确度,可以给定p,自行推导下

如图:


4、代码示例

4.1、通过文件实现(PHP代码为例)

<?php
/**
 * Class bloomFilterByFile
 * BloomFilter 过滤器
 * File 实现方式
 * 此方法比较low,因为使用的是文件,每次都是全部取出处理后再全部存入,会有较高的IO耗时
 * 可继续优化
 */
$obj = bloomFilterByFile::getInstance('bloomFilterByFile');
//var_dump($obj->add("test001"));
//var_dump($obj->exists("test001"));
//var_dump($obj->delFile());
class bloomFilterByFile {

    private static $instance;

    private $blockData;         //定义bit数组
    private $blockLength;       //定义bit数组的长度
    private $hashFunCnt = 4;    //hash函数个数,目的是为了构造函数里的计算
    private $fileDir    = "/tmp/bloomFilterByFile"; //保存文件路径
    private $filePath;

    /**
     * bloomFilterByFile constructor.
     * @param $productKey   //定义业务功能key,确保功能隔离
     * @param $dataCnt      //预计处理的元素个数
     * 已知hash函数个数:k
     * 已知需要处理的元素个数:n
     * 需要开辟的bit长度:m
     * 误差率:p
     * m = - (n * lnp) / ((ln2)^2)
     * k = m * ln2 / n
     * 本例中k=4;求得m=5.77n;求得p约为 6.3% 即 1000个数据中约有63个误差判断
     * 如果需要提高准确性,设定p的值,倒推mn关系,算出hash函数个数即可
     */
    private function __construct($productKey, $dataCnt) {
        $this->blockLength  = intval(13 * $this->hashFunCnt / 9) * $dataCnt;    //此处13 * n / 9 是一个相对合理的长度
        if(!is_dir($this->fileDir)){
            mkdir($this->fileDir, 0777);
        }
        $this->filePath     = $this->fileDir."/bloomFilter_".$productKey.".log";
        if(!file_exists($this->filePath)){
            file_put_contents($this->filePath, "");
        }
    }

    private function __clone(){}

    /**
     * @param $productKey
     * @param int $dataCnt
     * @return bloomFilterByFile|bool
     */
    public static function getInstance($productKey, $dataCnt = 10000){

        if(!$productKey){
            return false;//异常抛出:不存在对应的业务key
        }

        if(!preg_match("/^[0-9a-zA-Z_]{1,50}$/", $productKey)){
            return false;//异常抛出:命名不规范
        }

        if(!self::$instance){
            self::$instance = new bloomFilterByFile($productKey, $dataCnt);
        }

        return self::$instance;
    }

    /**
     * @param $value
     * @return array
     * hash 函数
     * 暂定4个hash函数
     */
    public function hashCode($value){
        $index = [];
        $index[] = intval(sprintf('%u', crc32($value))) % $this->blockLength;
        $index[] = intval(base_convert(substr(md5($value), 0, 8), 16, 10) % $this->blockLength);
        $index[] = intval(base_convert(substr(sha1($value), 0, 8), 16, 10)) % $this->blockLength;
        $index[] = intval(base_convert(bin2hex($value), 16, 10)) % $this->blockLength;
        return $index;
    }

    /**
     * 新增数据到过滤器内
     * 为了减小IO的操作,支持批量操作(未进行限制,不建议一次操作过多)
     * @param $values
     * @return bool
     */
    public function add($values){

        $values = is_array($values) ? $values : [$values];

        $fileGet = file_get_contents($this->filePath);

        $this->blockData = $fileGet ? @json_decode($fileGet, true) : array_fill(0, $this->blockLength, 0);;

        $indexs = [];
        foreach ($values as $value){
            $indexs = array_merge($indexs, self::hashcode($value));
        }

        if(empty($indexs)){
            return false;
        }

        foreach ($indexs as $index){
            $this->blockData[$index] = 1;
        }

        $filePut = file_put_contents($this->filePath, json_encode($this->blockData));
        if(false === $filePut){
            return false;
        }

        return true;
    }

    /**
     * 按照文件存储的方式查询一个value是否存在
     * @param $value
     * @return bool
     */
    public function exists($value){

        $fileGet = file_get_contents($this->filePath);

        if(!$fileGet){
            return false;
        }

        $this->blockData = @json_decode($fileGet, true);

        $indexs = self::hashcode($value);

        if(empty($indexs)){
            return false;
        }

        foreach ($indexs as $index){
            if(!$this->blockData[$index]){
                return false;
            }
        }
        return true;
    }

    /**
     * 清理记录的log文件
     * @return bool
     */
    public function delFile(){
        if(!file_exists($this->filePath)){
            return false;
        }
        return unlink($this->filePath);
    }
}

4.2、通过redis实现(利用redis天然自带的bit操作优势)

<?php
/**
 * Created by PhpStorm.
 * Date: 2019/4/18
 * Time: 10:16 AM
 * BloomFilter 过滤器
 * Redis 实现 利用redis天然的bit方法
 */
$obj = bloomFilterByRedis::getInstance("bloomFilterByRedis");
//var_dump($obj->add("test001"));
//var_dump($obj->exists("test001"));

class bloomFilterByRedis{

    private static $instance;

    private $redisConn;
    private $productKey;        //产品功能唯一key,作为文件名key或redis的key
    private $hashFunCnt = 4;    //hash函数个数
    private $blockLength;

    //构造方法,定义唯一的功能key、存储方式,目前只实现redis相关存储
    public function __construct($productKey, $dataCnt){
        $this->redisConn    = new Redis();
        if($this->redisConn->connect('127.0.0.1', 6379, 3) === false){
            //链接失败,异常提示
            return false;
        }
        $this->productKey   = $productKey;
        $this->blockLength  = intval(13 * $this->hashFunCnt / 9) * $dataCnt;
    }

    private function __clone(){}

    /**
     * @param $productKey
     * @param int $dataCnt  预计将会校验的元素数量
     * @return bool|Common_BloomFilterByRedis
     * 已知hash函数个数:k
     * 已知需要处理的元素个数:n
     * 需要开辟的bit长度:m
     * 误差率:p
     * m = - (n * lnp) / ((ln2)^2)
     * k = m * ln2 / n
     * 本例中k=4;求得m=5.77n;求得p约为 6.3% 即 1000个数据中约有63个误差判断
     * 如果需要提高准确性,设定p的值,倒推mn关系,算出hash函数个数即可
     */
    public static function getInstance($productKey, $dataCnt = 10000){

        if(!$productKey){
            return false;//异常抛出:不存在对应的业务key
        }

        if(!preg_match("/^[0-9a-zA-Z_]{1,50}$/", $productKey)){
            return false;//异常抛出:命名不规范
        }

        if(!self::$instance){
            self::$instance = new bloomFilterByRedis($productKey, $dataCnt);
        }

        return self::$instance;
    }

    /**
     * @param $value
     * @return array
     * hash 函数
     * 暂定4个hash函数
     */
    public function hashCode($value){
        $index = [];
        $index[] = intval(sprintf('%u', crc32($value))) % $this->blockLength;
        $index[] = intval(base_convert(substr(md5($value), 0, 8), 16, 10) % $this->blockLength);
        $index[] = intval(base_convert(substr(sha1($value), 0, 8), 16, 10)) % $this->blockLength;
        $index[] = intval(base_convert(bin2hex($value), 16, 10)) % $this->blockLength;
        return $index;
    }

    /**
     * @param $values
     * @return bool
     * 按照redis存储的方式新增value
     * todo::暂不可用,线上禁用
     */
    public function add($values){

        $values = is_array($values) ? $values : [$values];

        $indexs = [];
        foreach ($values as $value){
            $indexs = array_merge($indexs, $this->hashcode($value));
        }

        if(empty($indexs)){
            return false;
        }

        $indexs = array_unique($indexs);

        //开启redis区块操作
        $this->redisConn->multi();

        foreach ($indexs as $index){
            $this->redisConn->setBit($this->productKey, $index, 1); //第一次设置会返回0
        }

        $ret = $this->redisConn->exec();

        return is_array($ret) && $ret ? true : false;
    }

    /**
     * @param $value
     * @return bool
     * 通过redis存储的方式判断是否存在一个值
     * todo::暂不可用,线上禁用
     */
    public function exists($value){

        $indexs = $this->hashcode($value);

        if(empty($indexs)){
            return false;
        }

        //开启redis区块操作
        $transaction = $this->redisConn->multi();

        foreach ($indexs as $index){
            $transaction->getBit($this->productKey, $index);
        }

        $ret = $transaction->exec();

        foreach ($ret as $bit){
            if(!$bit){
                return false;
            }
        }

        return true;
    }
}