布隆过滤器

引言

在介绍布隆过滤器之前我们首先引入几个场景。

场景一

在一个高并发的计数系统中,如果一个key没有计数,此时我们应该返回0,但是访问的key不存在,相当于每次访问缓存都不起作用了。那么如何避免频繁访问数量为0的key而导致的缓存被击穿?

有人说, 将这个key的值置为0存入缓存不就行了吗?确实,这是一个好的方案。大部分情况我们都是这样做的,当访问一个不存在的key的时候,设置一个带有过期时间的标志,然后放入缓存。不过这样做的缺点也很明显,浪费内存和无法抵御随机key攻击。

场景二

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

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

一种解决思路

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

 

布隆过滤器原理

上面的思路其实就是布隆过滤器的思想,只不过因为hash函数的限制,多个字符串很可能会hash成一个值。为了解决这个问题,布隆过滤器引入多个hash函数来降低误判率。

下图表示有三个hash函数,比如一个集合中有x,y,z三个元素,分别用三个hash函数映射到二进制序列的某些位上,假设我们判断w是否在集合中,同样用三个hash函数来映射,结果发现取得的结果不全为1,则表示w不在集合里面。

布隆过滤器处理流程

布隆过滤器应用很广泛,比如垃圾邮件过滤,爬虫的url过滤,防止缓存击穿等等。下面就来说说布隆过滤器的一个完整流程,相信读者看到这里应该能明白布隆过滤器是怎样工作的。

第一步:开辟空间

开辟一个长度为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到某一位的概率为:

1m

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

p(1)=1m

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

p(0)=11m

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

p(0)=(11m)nk

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

p()=1(11m)nk

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

p()=(1(11m)nk)k

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

(11m)nk

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

limx(1+1x)x=e

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

 p()=(1(e)nk/m)k

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

p1p=ln(1etk)+klnet(etk)1etk

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

(1etk)ln(1etk)=etklnetk

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

(1/2)ln2mn,约等于0.6185m/n

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

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

 

m/nkk=1k=2k=3k=4k=5k=6k=7k=8
21.390.3930.400      
32.080.2830.2370.253     
42.770.2210.1550.1470.160    
53.460.1810.1090.0920.0920.101   
64.160.1540.08040.06090.05610.05780.0638  
74.850.1330.06180.04230.03590.03470.0364  
85.550.1180.04890.03060.0240.02170.02160.0229 
96.240.1050.03970.02280.01660.01410.01330.01350.0145
106.930.09520.03290.01740.01180.009430.008440.008190.00846
117.620.08690.02760.01360.008640.00650.005520.005130.00509
128.320.080.02360.01080.006460.004590.003710.003290.00314
139.010.0740.02030.008750.004920.003320.002550.002170.00199
149.70.06890.01770.007180.003810.002440.001790.001460.00129
1510.40.06450.01560.005960.0030.001830.001280.0010.000852
1611.10.06060.01380.0050.002390.001390.0009350.0007020.000574
1711.80.05710.01230.004230.001930.001070.0006920.0004990.000394
1812.50.0540.01110.003620.001580.0008390.0005190.000360.000275
1913.20.05130.009980.003120.00130.0006630.0003940.0002640.000194
2013.90.04880.009060.00270.001080.000530.0003030.0001960.00014
2114.60.04650.008250.002360.0009050.0004270.0002360.0001470.000101
2215.20.04440.007550.002070.0007640.0003470.0001850.0001127.46e-05
2315.90.04250.006940.001830.0006490.0002850.0001478.56e-055.55e-05
2416.60.04080.006390.001620.0005550.0002350.0001176.63e-054.17e-05
2517.30.03920.005910.001450.0004780.0001969.44e-055.18e-053.16e-05
26180.03770.005480.001290.0004130.0001647.66e-054.08e-052.42e-05
2718.70.03640.00510.001160.0003590.0001386.26e-053.24e-051.87e-05
2819.40.03510.004750.001050.0003140.0001175.15e-052.59e-051.46e-05
2920.10.03390.004440.0009490.0002769.96e-054.26e-052.09e-051.14e-05
3020.80.03280.004160.0008620.0002438.53e-053.55e-051.69e-059.01e-06
3121.50.03170.00390.0007850.0002157.33e-052.97e-051.38e-057.16e-06
3222.20.03080.003670.0007170.0001916.33e-052.5e-051.13e-055.73e-06

 

 

  简单实现:

/**
 * Implements a Bloom Filter
 */
class BloomFilter {
    /**
     * Size of the bit array
     *
     * @var int
     */
    protected $m;
 
    /**
     * Number of hash functions
     *
     * @var int
     */
    protected $k;
 
    /**
     * Number of elements in the filter
     *
     * @var int
     */
    protected $n;
 
    /**
     * The bitset holding the filter information
     *
     * @var array
     */
    protected $bitset;
 
    /**
     * 计算最优的hash函数个数:当hash函数个数k=(ln2)*(m/n)时错误率最小
     *
     * @param int $m bit数组的宽度(bit数)
     * @param int $n 加入布隆过滤器的key的数量
     * @return int
     */
    public static function getHashCount($m, $n) {
        return ceil(($m / $n) * log(2));
    }
 
    /**
     * Construct an instance of the Bloom filter
     *
     * @param int $m bit数组的宽度(bit数) Size of the bit array
     * @param int $k hash函数的个数 Number of different hash functions to use
     */
    public function __construct($m, $k) {

        $this->m = $m;
        $this->k = $k;
        $this->n = 0;
 
        /* Initialize the bit set */
        $this->bitset = array_fill(0, $this->m - 1, false);
    }
 
    /**
     * False Positive的比率:f = (1 – e-kn/m)k   
     * Returns the probability for a false positive to occur, given the current number of items in the filter
     *
     * @return double
     */
    public function getFalsePositiveProbability() {
        $exp = (-1 * $this->k * $this->n) / $this->m;
 
        return pow(1 - exp($exp),  $this->k);
    }
 
    /**
     * Adds a new item to the filter
     *
     * @param mixed Either a string holding a single item or an array of 
     *              string holding multiple items.  In the latter case, all
     *              items are added one by one internally.
     */
    public function add($key) {
        if (is_array($key)) {
            foreach ($key as $k) {
                $this->add($k);
            }
            return;
        }
 
        $this->n++;
 
        foreach ($this->getSlots($key) as $slot) {
            $this->bitset[$slot] = true;
        }
    }
 
    /**
     * Queries the Bloom filter for an element
     *
     * If this method return FALSE, it is 100% certain that the element has
     * not been added to the filter before.  In contrast, if TRUE is returned,
     * the element *may* have been added to the filter previously.  However with
     * a probability indicated by getFalsePositiveProbability() the element has
     * not been added to the filter with contains() still returning TRUE.
     *
     * @param mixed Either a string holding a single item or an array of 
     *              strings holding multiple items.  In the latter case the
     *              method returns TRUE if the filter contains all items.
     * @return boolean
     */
    public function contains($key) {
        if (is_array($key)) {
            foreach ($key as $k) {
                if ($this->contains($k) == false) {
                    return false;
                }
            }
 
            return true;
        }
 
        foreach ($this->getSlots($key) as $slot) {
            if ($this->bitset[$slot] == false) {
                return false;
            }
        }
 
        return true;
    }
 
    /**
     * Hashes the argument to a number of positions in the bit set and returns the positions
     *
     * @param string Item
     * @return array Positions
     */
    protected function getSlots($key) {
        $slots = array();
        $hash = self::getHashCode($key);
        mt_srand($hash);
 
        for ($i = 0; $i < $this->k; $i++) {
            $slots[] = mt_rand(0, $this->m - 1);
        }
 
        return $slots;
    }
 
    /**
     * 使用CRC32产生一个32bit(位)的校验值。
     * 由于CRC32产生校验值时源数据块的每一bit(位)都会被计算,所以数据块中即使只有一位发生了变化,也会得到不同的CRC32值。
     * Generates a numeric hash for the given string
     *
     * Right now the CRC-32 algorithm is used.  Alternatively one could e.g.
     * use Adler digests or mimick the behaviour of Java's hashCode() method.
     *
     * @param string Input for which the hash should be created
     * @return int Numeric hash
     */
    protected static function getHashCode($string) {
        return crc32($string);
    }
    
}
 
 
 
$items = array("first item", "second item", "third item");
        
/* Add all items with one call to add() and make sure contains() finds
 * them all.
 */
$filter = new BloomFilter(100, BloomFilter::getHashCount(100, 3));
// var_dump($filter); exit;
$filter->add($items);
 
// var_dump($filter); exit;
$items = array("firsttem", "seconditem", "thirditem");
foreach ($items as $item) {
 var_dump(($filter->contains($item)));
}




 
/* Add all items with multiple calls to add() and make sure contains()
* finds them all.
*/
$filter = new BloomFilter(100, BloomFilter::getHashCount(100, 3));
foreach ($items as $item) {
    $filter->add($item);
}
$items = array("fir sttem", "secondit em", "thir ditem");

foreach ($items as $item) {
 var_dump(($filter->contains($item)));
}

 

转载于:https://www.cnblogs.com/xingxia/p/bool_filter.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值