首先看下面几个场景:
- 字处理软件中,需要检查一个英语单词是否拼写正确
- 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上
- 网页爬虫对URL的去重,避免爬取相同的URL地址
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
- 缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。
以上场景归纳为一个问题就是:如何在一个很大数据集合中迅速查询一个元素?
有人会说,我们可以直接把数据放在redis缓存或者存在数据库,查询的时候直接匹配就不就ok?当数据量比较小,我们内存够大的时候的确可以这样处理,甚至可以直接用HashSet、HashMap解决,但是如果我们数据量很大,几千万或者几亿,这种情况下又如何处理呢?布隆过滤器就应运而生。
基本概念
布隆过滤器实际上是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希),作用就在于能跟迅速判断一个元素是否在一个集合中,且查询效率很高(1-N,最优能近于1)。
哈希函数
首先简单介绍下哈希函数:将任意大小的数据转换成特定大小的数据的函数,转换后的数据称为哈希值或哈希编码。比如我们常用的md5。下面一副示意图:
原始数据与哈希编码的映射是近乎一对一的(非常非常低的几率下两个不同key的hash值是可能重复的)。哈希函数是实现哈希表和布隆过滤器的基础。
布隆过滤器原理:
初始状态下是一个m位的全为0的bit数组,以及k个hash函数,如下图所示
假定我们要维护的集合为{N1, N2},首先输入N1,经过hash函数f1(N1)%m得到2,f2(N1)%m得到5,那么就将bit数组的2,5位置改为1,如下图所示:
同理计算N2:
此时,如果我们要查询一个元素N3,判断N3是否在集合{N1, N2}中,我们只需要进行f1(N3)%m,f2(N3)%m的计算:如果f1(N3)%m,f2(N3)%m对应下标的值有一个为0,那就说明元素不在集合内,反之如果两个值都为1,则说明元素在集合内。但实际上存在元素不在集合中却对应都为1的情况,这就是误判率的存在。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
通常我们需要根据实际业务来计算误判率,确定m/n和k的值(m代表bit数组长度,n代表集合元素数,k代表hash函数个数),具体可以参考这篇文章。
由上我们可以看出布隆过滤器的优点就是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难(一般情况下是不能删除的)。
php和redis实现bloom filter方法
第一步,获取几个hash函数,比如BKDRHash,JSHash,RSHash等等。这些hash函数我们直接获取就可以了。
/**函数集合类
*/
class HashSet {
const JSHASH = 'JSHash';
const BKDRHASH = 'BKDRHash';
const RSHASH = 'RSHash';
const PJWHASH = 'PJWHash';
const ELFHash = 'ELFHash';
const BKDRHash = 'BKDRHash';
const SDBMHash = 'SDBMHash';
const DJBHash = 'DJBHash';
const DEKHash = 'DEKHash';
const FNVHash = 'FNVHash';
//函数具体实现不详述了
public static function JSHash($string) {}
public static function BKDRHash($string) {}
public static function RSHash($string) {}
public static function BKDRHash($string) {}
public static function SDBMHash($string) {}
...
}
第二步,使用redis的setBit和getBit操作来实现过滤器。当然我们也可以用php作位运算,但是比较麻烦这里不赘述了。值得注意的是setBit和getBit必须2.2以上版本的redis才能支持。
/**使用redis实现的布隆过滤器
*/
abstract class BloomFilterBase {
//bit数组
protected $bit_array_name;
protected $bit_array_length;
//hash函数集合
protected $hash_set;
protected $redis_obj;
public function __construct() {
if (!$this->hash_set) {
$this->hash_set = HashSet::$hash_set;
}
$this->redis_obj = new Redis;
}
/**定义bitArray
*/
public function setBitArray($name, $lengh) {
$this->bit_array_name = $name;
$this->bit_array_lengh = $length;
}
/**添加元素到集合
*/
public function add ($string) {
if (!$this->bit_array_name || !$this->bit_array_length) {
throw new Exception("需要预定义bitArray", 1);
}
foreach ($this->hash_set as $function) {
$hash = HashSet::$function($string) % $this->bit_array_length;
$this->redis_obj->setBit($this->bit_array_name, $hash, 1);
}
}
/**判断是否存在
*/
public function isExists($string) {
if (!$this->bit_array_name || !$this->bit_array_length) {
throw new Exception("需要预定义bitArray", 1);
}
foreach ($this->hash_set as $function) {
$hash = HashSet::$function($string) % $this->bit_array_length;
$res = $this->redis_obj->getBit($this->bit_array_name, $hash);
if ($res == 0) {
return false;
}
}
return true;
}
}
第三步,上面定义的是一个抽象类,可以根据具体的业务来使用。
/**
* 重复内容过滤器
* 该布隆过滤器总位数为2^32位, 判断条数为2^30条. hash函数最优为3个.(能够容忍最多的hash函数个数)
* 使用的三个hash函数为
* BKDR, SDBM, JSHash
*
* 注意, 在存储的数据量到2^30条时候, 误判率会急剧增加, 因此需要定时判断过滤器中的位为1的的数量是否超过50%, 超过则需要清空.
*/
class FilteRepeatedComments extends BloomFilterBase
{
protected $bit_array_name = 'repeated_comments_bloom_filter';
protected $bit_array_length = 0xFFFFFFFF;
protected $hash_set = array(
HashSet::JSHASH,
HashSet::BKDRHASH,
HashSet::RSHASH,
);
}