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