快取現在在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過濾
- 防止快取擊穿
好了,就介紹到這裡啦!如有興趣,還有布穀鳥過濾器。