從快取穿透聊到布隆過濾器

13sai發表於2019-10-30

快取現在在web領域應用廣泛,相信大部分開發人員都會用到,然而你遇見過快取穿透嗎?

快取穿透是指查詢一個根本不存在的資料,快取層和儲存層都不會命中,但是出於容錯的考慮,如果從儲存層查不到資料則不寫入快取層。
簡單說,就查詢一個不存在的key,因為沒有快取,就會去資料庫查詢,從而達到穿透快取。增大資料庫壓力的險惡目的。
一般來說,不是惡意操作,正常來說,不會遇到這樣的問題,然而,怕的就是一些險惡用心的攻擊者。那麼,我們如何有效處理這種問題呢?
簡單想一下,如果我們把有效的key集合起來,查詢之前我們先判斷一下查詢的key是否在集合中,如果不在,直接打回去,讓你調皮。這個問題不就解決了嗎?
但是,如果真的先把所有key組成集合,那這個儲存佔用的記憶體太大了,當有1億個key,那儲存空間也是相當可觀的,有點太過浪費了。
為了不浪費,我跟你說,有一個小玩意叫“布隆過濾器”,它能幫你節省空間,節省錢。

布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。
按上面的問題,我們可以把所有的key通過特定的演算法,儲存到這種二進位制向量中,我們來看網上的一張圖。

從快取穿透聊到布隆過濾器

我們可以通過三個雜湊演算法將w儲存到二進位制向量,當查詢w是否存在時,我們可以再通過這三個演算法,如果演算法算出來所在的位置均為1,則表示w可能存在(注意是可能存在),否則一定不存在。(這個很好理解,我就不做說明了)

說了這麼多,我們來嘗試下程式碼:

<?php
/**
 * Author: sai
 * Date: 2019/5/17
 * Time: 14:10
 * 程式碼來自網路,有改動
 */
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));
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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));
    }
//    var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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;
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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]));
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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);
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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]);
        }
        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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 = (int) (($hash << 5) ^ ($hash >> 27)) ^ ord($string[$i]);
        }
//        var_dump((int)($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        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]);
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }
}

/**
 * 使用redis實現的布隆過濾器
 */
abstract class BloomFilterRedis
{
    /**
     * 需要使用一個方法來定義bucket的名字
     */
    protected $bucket;

    protected $hashFunction;

    public function __construct()
    {
        if (!$this->bucket || !$this->hashFunction) {
            throw new Exception("需要定義bucket和hashFunction", 1);
        }
        $this->Hash = new BloomFilterHash;
        $this->Redis = self::getRedis(); //假設這裡你已經連線好了
    }

    public static function getRedis()
    {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
//        $redis->auth('13sai666.');
//        var_dump($redis->info('SERVER'));die;
        $redis->select(7);
        return $redis;
    }

    /**
     * 新增到集合中
     */
    public function add($string)
    {
        foreach ($this->hashFunction as $function) {
            $hash = $this->Hash->$function($string);
            $this->Redis->setBit($this->bucket, $hash, 1);
        }
        return true;
    }

    /**
     * 查詢是否存在, 存在的一定會存在, 不存在有一定機率會誤判
     */
    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();
//        var_dump($res);
        foreach ($res as $bit) {
            if ($bit == 0) {
                return false;
            }
        }
        return true;
    }
}

/**
 * 重複內容過濾器
 * 該布隆過濾器總位數為2^32位, 判斷條數為2^30條. hash函式最優為3個.(能夠容忍最多的hash函式個數)
 *
 * 注意, 在儲存的資料量到2^30條時候, 誤判率會急劇增加, 因此需要定時判斷過濾器中的位為1的的數量是否超過50%, 超過則需要清空.
 */
class FilteRepeatedComments extends BloomFilterRedis
{
    /**
     * 表示判斷重複內容的過濾器
     * @var string
     */
    protected $bucket = 'bulong';

    protected $hashFunction = ['FNVHash', 'JSHash', 'ELFHash'];
}

可以呼叫測試下,

var_dump((new FilteRepeatedComments())->add('abc')); //true
var_dump((new FilteRepeatedComments())->add('bcd'));//true
var_dump((new FilteRepeatedComments())->add('dfg'));//true
var_dump((new FilteRepeatedComments())->exists('dfg'));//true
var_dump((new FilteRepeatedComments())->exists('dgg'));//false

簡單的測試通過!
當然,應用時需要進行更多的測試。
那麼這個叫“布隆過濾器”的東東真有那麼好用?
我相信,你應該可以看出來,這是有誤判率的,另外刪除也是困難的。

具體誤判推導過程,可以參考:
布隆過濾器(bloom filter)介紹以及php和redis實現布隆過濾器實現方法
布隆過濾器簡介 - Jack47 - 部落格園

應用場景:

  • 垃圾郵件過濾
  • 爬蟲的url過濾
  • 防止快取擊穿

好了,就介紹到這裡啦!如有興趣,還有布穀鳥過濾器。

部落格地址:http://blog.13sai.com/essay/188

相關文章