引言
在介紹布隆過濾器之前我們首先引入幾個場景。
場景一
在一個高併發的計數系統中,如果一個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到某一位的概率為:
1?
也就是說當前位被反轉為1的概率:
?(1)=1?
那麼這一位沒有被反轉的概率為:
?(0)=1−1?
假設我們存入n各元素,使用k個hash函式,此時沒有被翻轉的概率為:
?(0)=(1−1?)??
那什麼情況下我們會誤判呢,就是原本不應該被翻轉的位,結果翻轉了,也就是
?(誤判)=1−(1−1?)??
由於只有k個hash函式同時誤判了,整體才會被誤判,最後誤判的概率為
?(誤判)=(1−(1−1?)??)?
要使得誤判率最低,那麼我們需要求誤判與m、n、k之間的關係,現在假設m和n固定,我們計算一下k。可以首先看看這個式子:
(1−1?)??
由於我們的m很大,通常情況下我們會用2^32來作為m的值。上面的式子中含有一個重要極限
lim?→∞(1+1?)?=?
因此誤判率的式子可以寫成
?(誤判)=(1−(?)−??/?)?
接下來令?=−?/?,兩邊同時取對數,求導,得到:
?′1?=??(1−???)+?????(−???)1−???
讓?′=0,則等式後面的為0,最後整理出來的結果是
(1−???)??(1−???)=????????
計算出來的k為??2??,約等於0.693??,將k代入p(誤判),我們可以得到概率和m、n之間的關係,最後的結果
(1/2)??2??,約等於0.6185?/?
以上我們就得出了最佳hash函式個數以及誤判率與mn之前的關係了。
下表是m與n比值在k個hash函式下面的誤判率
php+Redis實現的布隆過濾器
由於Redis實現了setbit和getbit操作,天然適合實現布隆過濾器,redis也有布隆過濾器外掛。這裡使用php+redis實現布隆過濾器。
首先定義一個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');
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結