布隆过滤器及实现方法攻略
什么是布隆过滤器?
布隆过滤器是一种非常实用的数据结构,它可以用于快速判断一个元素是否在一个集合中。布隆过滤器可以有效地降低查询一个元素是否在集合中的时间复杂度,但是会带来一定的误判率。它由早在1970年提出,以其高效的查询速度和内存占用率低的特点而广受欢迎,被广泛应用于网络爬虫等场景中。
布隆过滤器的实现原理
布隆过滤器采用的是概率算法,基于哈希函数和位图实现。它的核心思想是:开辟一个足够大的位图,对于每个元素,将其经过多个哈希函数得到的哈希值所对应的位置在位图上标记为1。当需要判断某个元素是否在集合中时,将其经过同样的哈希函数得到的哈希值所对应的位图位置查询一遍,如果所有的位都是1,则认为该元素在集合中。
布隆过滤器的应用场景
布隆过滤器可以用于以下场景:
- 网络爬虫的URL去重,防止重复采集;
- 黑名单过滤,例如垃圾邮件过滤等;
- 预判缓存命中,避免缓存miss带来的性能损失。
在这些场景中,布隆过滤器的作用都是用来减少需要进行高代价的副作用操作,例如网络爬虫需要访问目标网站进行URL去重,较大规模的黑名单过滤等。
PHP实现布隆过滤器
PHP实现布隆过滤器比较简单,我们可以使用常见的位图算法,将标记位存储在一个整数中。下面是一个PHP版的布隆过滤器代码:
class BloomFilter
{
public static $hashFuncList = [
// 自定义哈希函数,可以维护多个哈希函数。
'hash1',
'hash2',
];
protected $bitmap = [];
public function __construct($size = 1024)
{
$this->bitmap = array_fill(0, $size, 0);
}
public function add($word)
{
foreach(self::$hashFuncList as $func) {
$hash = $this->$func($word);
$idx = $hash % count($this->bitmap);
$this->bitmap[$idx] = 1;
}
}
public function exists($word)
{
foreach(self::$hashFuncList as $func) {
$hash = $this->$func($word);
$idx = $hash % count($this->bitmap);
if($this->bitmap[$idx] != 1) {
return false;
}
}
return true;
}
protected function hash1($str)
{
// 自定义哈希函数
return hash('md5', $str);
}
protected function hash2($str)
{
// 自定义哈希函数
return hash('sha256', $str);
}
}
在这个实现中,我们使用了一个 $hashFuncList 数组来维护多个哈希函数。每次添加或查找元素时,我们都会对这个元素进行多次哈希,并在对应位置上打标记。查询元素是否在集合中时,需要将其对应的位置都查询一遍,如果所有位置都被打标记,则该元素可能在集合中。
Redis实现布隆过滤器
在Redis中,我们可以使用自带的 bitmap
数据类型实现布隆过滤器,不需要再手写一个简单的位图算法了。其实现方式与PHP类似,仍然是使用多个哈希函数对元素进行哈希,并将结果在位图上标记为1。下面是一个Redis版的布隆过滤器代码:
class RedisBloomFilter
{
protected $redis = null;
public function __construct($redis)
{
$this->redis = $redis;
}
public function add($word)
{
foreach(['hash1', 'hash2'] as $func) {
$hash = $this->$func($word);
$this->redis->setbit('bloom', $hash, 1);
}
}
public function exists($word)
{
foreach(['hash1', 'hash2'] as $func) {
$hash = $this->$func($word);
if($this->redis->getbit('bloom', $hash) == 0) {
return false;
}
}
return true;
}
protected function hash1($str)
{
return hexdec(substr(md5($str), 0, 8)) % 100000;
}
protected function hash2($str)
{
return hexdec(substr(md5($str), 8, 8)) % 100000;
}
}
在这个实现中,我们使用了Redis自带的setbit
和getbit
命令分别用于设置和读取位图上的某一位。当需要添加或查询一个元素时,我们需要依次调用两个自定义的哈希函数来得到需要修改或查询的位图偏移量,并将其设置为1或查询其是否为1。
示例应用
下面我们来简单了解一下如何应用布隆过滤器解决具体问题:
防止缓存穿透
当缓存中不存在某个key时,我们需要向后端存储系统请求数据并将其缓存到缓存中。但在这个时候,有可能会出现频繁请求一个不存在的key的情况,这就会导致不断地向后端请求和缓存未命中的key,这就是缓存穿透问题。如果数据规模很大,那么查询后端存储系统的代价就会很大。这个时候我们就可以使用布隆过滤器来解决这个问题:
$bloom = new BloomFilter(100000);
$key = $_GET['key'];
if(!$bloom->exists($key)) {
// key可能不存在,需要进一步查询后端存储系统
// ...
// 查询结果放到缓存中,并将key加入布隆过滤器中
$bloom->add($key);
}
URL去重
在爬虫过程中,经常需要处理大量的URL,如果不进行去重,可能会大幅增加爬虫工作量。布隆过滤器既可以用来过滤已抓取或处理过的URL,也可以用来过滤黑名单URL。下面是一个基于Redis的示例代码:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 600);
$bloom = new RedisBloomFilter($redis);
function isDuplicateUrl($url) {
global $bloom;
if ($bloom->exists($url)) {
return true; // 该URL已经被处理过了
}
// 根据URL进行一些处理,并把处理后的结果存入缓存中
// ...
// 将处理过的URL加入布隆过滤器缓存中
$bloom->add($url);
return false;
}
在这个实现中,我们在爬虫过程中使用isDuplicateUrl
函数判断当前URL是否已经被处理过,如果已经被处理过则直接跳过,否则将其加入Redis布隆过滤器并进行处理。这样,就能够有效地去重URL并避免重复处理。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:布隆过滤器(bloom filter)及php和redis实现布隆过滤器的方法 - Python技术站