布隆过滤器原理以及如何用php+redis实现布隆过滤器

Posted 成为一名架构师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了布隆过滤器原理以及如何用php+redis实现布隆过滤器相关的知识,希望对你有一定的参考价值。

在很多实际的业务逻辑开发中,经常会存在判断是否存在这种业务问题,我们首先看看下面的几种场景。

使用场景:

1、有的黑客为了让服务宕机,他们会构建大量不存在于缓存中的key向服务器发起请求,在数据量足够大的情况下,频繁的数据库查询可能导致DB挂掉。布隆过滤器很好的解决了缓存击穿的问题。

2、反垃圾邮件,从数十亿个垃圾邮件列表中判断某个邮箱是否是垃圾邮箱。

4、...............................


比如一个邮件系统,我们需要设置黑名单用户,当判断垃圾邮件的时候,要怎么去做。比如爬虫系统,我们要记录下来已经访问过的链接避免下次访问重复的链接。

在邮件很少或者用户很少的情况下,我们用普通数据库自带的查询就能完成。在数据量太多的时候,为了保证速度,通常情况下我们会将结果缓存到内存中,数据结构用hash表。这种查找的速度是O(1),但是内存消耗也是惊人的。打个比方,假如我们要存10亿条数据,每条数据平均占据32个字节,那么需要的内存是64G,这已经是一个惊人的大小了。

一种解决思路

能不能有一种思路,查询的速度是O(1),消耗内存特别小呢?前辈门早就想出了一个很好的解决方案。由于上面说的场景判断的结果只有两种状态(是或者不是,存在或者不存在),那么对于所存的数据完全可以用位来表示!数据本身则可以通过一个hash函数计算出一个key,这个key是一个位置,而这个key所对的值就是0或者1(因为只有两种状态),如下图:

布隆过滤器原理

一个空的布隆过滤器是由m个bits组成的bit array,每一个bit位都初始为0。并且定义有k个不同的哈希函数,每个哈希函数都将元素哈希到bit array的不同位置。

当添加一个元素时,用k个哈希函数分别将它hash得到k个bit位,然后将这些bit位置位1。

查询一个函数时,同样用k个哈希函数将它hash,再判断k个bit位上是否都为1,如果其中某一位为0,则该元素不存在于布隆过滤器中。

常规的布隆过滤器不允许执行删除元素操作,因为那样会把k个bits位置位0,而其中某一位可能和其他元素想对应。因此删除操作会引入false negative,如果需要删除操作可以使用Counting Bloom Filter


布隆过滤器原理以及如何用php+redis实现布隆过滤器

当k很大时,设计k个独立的哈希函数是不现实的。对于一个输出范围很大的哈希函数(MD5产生的128 bits),如果不同bits的相关性很小,则可以把此输出分割位k份。或者将k个不同的初始值结合元素,feed给一个哈希函数从而产生k个不同的值。

布隆过滤器处理流程

第一步:开辟空间

开辟一个长度为m的位数组(或者称二进制向量),这个不同的语言有不同的实现方式,甚至你可以用文件来实现。

第二步:寻找hash函数

获取几个hash函数,前辈们已经发明了很多运行良好的hash函数,比如BKDRHash,JSHash,RSHash等等。这些hash函数我们直接获取就可以了。

第三步:写入数据

将所需要判断的内容经过这些hash函数计算,得到几个值,比如用3个hash函数,得到值分别是1000,2000,3000。之后设置m位数组的第1000,2000,3000位的值位二进制1。

第四步:判断

接下来就可以判断一个新的内容是不是在我们的集合中。判断的流程和写入的流程是一致的。

误判问题

布隆过滤器虽然很高效(写入和判断都是O(1),所需要的存储空间极小),但是缺点也非常明显,那就是会误判。当集合中的元素越来越多,二进制序列中的1的个数越来越多的时候,判断一个字符串是否在集合中就很容易误判,原本不在集合里面的字符串会被判断在集合里面。

数学推导

布隆过滤器原理十分简单,但是hash函数个数怎么去判断,误判率有多少?

假设二进制序列有m位,那么经过当一个字符串hash到某一位的概率为:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

也就是说当前位被反转为1的概率:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

那么这一位没有被反转的概率为:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

假设我们存入n各元素,使用k个hash函数,此时没有被翻转的概率为:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

那什么情况下我们会误判呢,就是原本不应该被翻转的位,结果翻转了,也就是

布隆过滤器原理以及如何用php+redis实现布隆过滤器

由于只有k个hash函数同时误判了,整体才会被误判,最后误判的概率为

布隆过滤器原理以及如何用php+redis实现布隆过滤器

要使得误判率最低,那么我们需要求误判与m、n、k之间的关系,现在假设m和n固定,我们计算一下k。可以首先看看这个式子:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

由于我们的m很大,通常情况下我们会用2^32来作为m的值。上面的式子中含有一个重要极限

布隆过滤器原理以及如何用php+redis实现布隆过滤器

因此误判率的式子可以写成

布隆过滤器原理以及如何用php+redis实现布隆过滤器

接下来令 t=−n/m,两边同时取对数,求导,得到:

布隆过滤器原理以及如何用php+redis实现布隆过滤器

p′=0,则等式后面的为0,最后整理出来的结果是

布隆过滤器原理以及如何用php+redis实现布隆过滤器

计算出来的k为 ln2mn,约等于 0.693mn,将k代入p(误判),我们可以得到概率和m、n之间的关系,最后的结果

以上我们就得出了最佳hash函数个数以及误判率与mn之前的关系了。

下表是m与n比值在k个hash函数下面的误判率

php+Redis实现的布隆过滤器

由于Redis实现了setbit和getbit操作,天然适合实现布隆过滤器,这里使用php+redis实现布隆过滤器。

Redis的setbit 可以理解是一个位数组,至于这个数组有多大,redis中bit映射被限制在512MB之内,所以最大是2^32,也就是在这样一个位数组上存0或者是1 。

首先我们定义一个hash函数集合类,这些hash函数不一定都用到,实际上32位hash值的用3个就可以了,具体的数量可以根据你的位序列总量和你需要存入的量决定,上面已经给出最佳值

class BloomFilterHash{ /** * 由Justin Sobel编写的按位散列函数 */ public function JSHash($string, $len = null){ $hash = 1315423911; $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash ^= (($hash << 5) + ord($string[$i]) + ($hash >> 2)); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 该哈希算法基于AT&T贝尔实验室的Peter J. Weinberger的工作。 * Aho Sethi和Ulman编写的“编译器(原理,技术和工具)”一书建议使用采用此特定算法中的散列方法的散列函数。 */ public function PJWHash($string, $len = null){ $bitsInUnsignedInt = 4 * 8; //(unsigned int)(sizeof(unsigned int)* 8); $threeQuarters = ($bitsInUnsignedInt * 3) / 4; $oneEighth = $bitsInUnsignedInt / 8; $highBits = 0xFFFFFFFF << (int) ($bitsInUnsignedInt - $oneEighth); $hash = 0; $test = 0; $len || $len = strlen($string); for($i=0; $i<$len; $i++) { $hash = ($hash << (int) ($oneEighth)) + ord($string[$i]); } $test = $hash & $highBits; if ($test != 0) { $hash = (($hash ^ ($test >> (int)($threeQuarters))) & (~$highBits)); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 类似于PJW Hash功能,但针对32位处理器进行了调整。它是基于UNIX的系统上的widley使用哈希函数。 */ public function ELFHash($string, $len = null){ $hash = 0; $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash = ($hash << 4) + ord($string[$i]); $x = $hash & 0xF0000000; if ($x != 0) { $hash ^= ($x >> 24); } $hash &= ~$x; } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 这个哈希函数来自Brian Kernighan和Dennis Ritchie的书“The C Programming Language”。 * 它是一个简单的哈希函数,使用一组奇怪的可能种子,它们都构成了31 .... 31 ... 31等模式,它似乎与DJB哈希函数非常相似。 */ public function BKDRHash($string, $len = null){ $seed = 131; # 31 131 1313 13131 131313 etc.. $hash = 0; $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash = (int) (($hash * $seed) + ord($string[$i])); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 这是在开源SDBM项目中使用的首选算法。 * 哈希函数似乎对许多不同的数据集具有良好的总体分布。它似乎适用于数据集中元素的MSB存在高差异的情况。 */ public function SDBMHash($string, $len = null){ $hash = 0; $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash = (int) (ord($string[$i]) + ($hash << 6) + ($hash << 16) - $hash); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 由Daniel J. Bernstein教授制作的算法,首先在usenet新闻组comp.lang.c上向世界展示。 * 它是有史以来发布的最有效的哈希函数之一。 */ public function DJBHash($string, $len = null){ $hash = 5381; $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash = (int) (($hash << 5) + $hash) + ord($string[$i]); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。 */ public function DEKHash($string, $len = null){ $len || $len = strlen($string); $hash = $len; for ($i=0; $i<$len; $i++) { $hash = (($hash << 5) ^ ($hash >> 27)) ^ ord($string[$i]); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }
/** * 参考 http://www.isthe.com/chongo/tech/comp/fnv/ */ public function FNVHash($string, $len = null){ $prime = 16777619; //32位的prime 2^24 + 2^8 + 0x93 = 16777619 $hash = 2166136261; //32位的offset $len || $len = strlen($string); for ($i=0; $i<$len; $i++) { $hash = (int) ($hash * $prime) % 0xFFFFFFFF; $hash ^= ord($string[$i]); } return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF; }}

接着就是连接redis来进行操作

/** * 使用redis实现的布隆过滤器 */abstract class BloomFilterRedis{ /** * 需要使用一个方法来定义bucket的名字 */ protected $bucket;  protected $hashFunction;  public function __construct($config, $id){ if (!$this->bucket || !$this->hashFunction) { throw new Exception("需要定义bucket和hashFunction", 1); } $this->Hash = new BloomFilterHash; $this->Redis = new YourRedis; //假设这里你已经连接好了 }
/** * 添加到集合中 */ public function add($string){ $pipe = $this->Redis->multi(); foreach ($this->hashFunction as $function) { $hash = $this->Hash->$function($string); $pipe->setBit($this->bucket, $hash, 1); } return $pipe->exec(); }
/** * 查询是否存在, 存在的一定会存在, 不存在有一定几率会误判 */ public function exists($string){ $pipe = $this->Redis->multi(); $len = strlen($string); foreach ($this->hashFunction as $function) { $hash = $this->Hash->$function($string, $len); $pipe = $pipe->getBit($this->bucket, $hash); } $res = $pipe->exec(); foreach ($res as $bit) { if ($bit == 0) { return false; } } return true; }
}

上面定义的是一个抽象类,如果要使用,可以根据具体的业务来使用。比如下面是一个过滤重复内容的过滤器。

/** * 重复内容过滤器 * 该布隆过滤器总位数为2^32位, 判断条数为2^30条. hash函数最优为3个.(能够容忍最多的hash函数个数) * 使用的三个hash函数为 * BKDR, SDBM, JSHash * 注意, 在存储的数据量到2^30条时候, 误判率会急剧增加, 因此需要定时判断过滤器中的位为1的的数量是否超过50%, 超过则需要清空. */class FilteRepeatedComments extends BloomFilterRedis{ /** * 表示判断重复内容的过滤器 * @var string */ protected $bucket = 'rptc';
protected $hashFunction = array('BKDRHash', 'SDBMHash', 'JSHash');}


推荐阅读:



以上是关于布隆过滤器原理以及如何用php+redis实现布隆过滤器的主要内容,如果未能解决你的问题,请参考以下文章

Redis详解(十三)------ Redis布隆过滤器

布隆过滤器

redis 十五. 缓存穿透 与布隆过滤器原理

实战问题-- 布隆过滤器的三种实践:手写,Redission以及Guava

布隆过滤器原理及实现

详解布隆过滤器的原理和实现