快取穿透
概念
快取穿透是指查詢一個一定不存在的資料,由於快取是不命中時需要從DB查詢,查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到資料庫去查詢,造成快取穿透。
解決方案
- 採用布隆過濾器,使用一個足夠大的bitmap,用於儲存可能訪問的key,不存在的key直接被過濾
bloomfilter 就類似於一個 hash set ,用於快速判斷某個元素是否存在於集合中,其典型的應用場景就是快速判斷一個key是否存在於某容器,不存在就直接返回,布隆過濾器的關鍵就在於hash演算法和容器大小。
和一般的的hash set不同的是,這個演算法無需儲存key的值,對於每個key,只需要k個位元位,每個儲存一個標誌,用於判斷key是否在集合中。
演算法:
第一步:開闢空間
開闢一個長度為m的位陣列(或者稱二進位制向量),這個不同的語言有不同的實現方式,甚至你可以用檔案來實現。
第二步:尋找hash函式
獲取幾個hash函式,前輩們已經發明瞭很多執行良好的hash函式,比如BKDRHash,JSHash,RSHash等等。這些hash函式我們直接獲取就可以了。
第三步:寫入資料
將所需要判斷的內容經過這些hash函式計算,得到幾個值,比如用3個hash函式,得到值分別是1000,2000,3000。之後設定m位陣列的第1000,2000,3000位的值位二進位制
第四步:判斷
接下來就可以判斷一個新的內容是不是在我們的集合中。判斷的流程和寫入的流程是一致的。
優點:不需要儲存key,節省空間
缺點:1.演算法判斷key在集合中,有一定概率key其實不在集合中
2.無法刪除
典型的應用場景:
某些儲存系統的設計,會存在空查詢缺陷:當查詢一個不存在的key時,需要訪問慢裝置,導致效率低下。
比如一個前端頁面的快取系統,可能這樣設計:先查詢某個頁面在本地是否存在,如果存在就直接返回,如果不存在,就從後端獲取。但是當頻繁從快取系統查詢一個頁面時,快取系統將會頻繁請求後端,把壓力匯入後端。
這是隻要增加一個bloom演算法的服務,後端插入一個key時,在這個服務中設定一次
需要查詢後端時,先判斷key在後端是否存在,這樣就能避免後端的壓力。布隆過濾器是由布隆在1970年提出的,他實際上是由一個很長的二進位制向量和一些列隨機對映函式組成。布隆過濾器可以用於檢索一個元素是否在一個集合中。
它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率(假正例False positives,即Bloom Filter報告某一元素存在於某集合中,但是實際上該元素並不在集合中)和刪除困難,但是沒有識別錯誤的情形(即假反例False negatives,如果某個元素確實沒有在該集合中,那麼Bloom Filter 是不會報告該元素存在於集合中的,所以不會漏報)。
- 訪問key未來在DB查詢到值,也可將空值寫進快取,但可以設定較短過期時間
如果一個查詢返回的資料為空(不管是資料不存在,還是系統故障),我們仍然把這個空結果進入快取,但它的過期時間會很短,最長不超過五分鐘。
快取空物件會有兩個問題
第一,空值做了快取,意味著快取層中存了更多的鍵,需要更多的記憶體空間,比較有效的方法是針對這類資料設定一個較短的過期時間,讓其自動剔除。
快取層和儲存層的資料會有一段時間視窗的不一致,可能會對業務有一定影響。例如過期時間設定為5分鐘,如果此時儲存層新增了這個資料,那此段時間就會出現快取層和儲存層資料的不一致,此時可以利用訊息系統或其他方式清除掉快取層中的空物件。
快取雪崩
概念
大量的key設定了相同的過期時間,導致快取在同一時刻全部失效,造成瞬間DB請求量大,壓力驟增,引起雪崩
解決方案
可以在快取設定過期時間加上一個隨機值時間,使得每個key的過期時間分不開來,不會集中在同一時刻失效。
這個沒有完美解決方法,但是可以分析使用者行為,儘量讓失效時間點均勻分佈。大多數系統設計者考慮加鎖或者佇列的方式保證快取的單執行緒寫,從而能避免失效時大量的併發請求落到底層儲存系統上。
解決方法
1.加鎖排隊,限流--限流演算法。
1.計數2.滑動視窗3.令牌桶4.漏桶
在快取失效後,通過加鎖或者佇列來控制讀資料庫謝快取的執行緒數量。比如對某個key只允許一個執行緒查詢資料和寫快取,其他執行緒等待。
業界比較常用的做法,是使用mutex。簡單地來說,就是快取失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用快取工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作並回設快取;否則,就重試整個get快取的方法。
SETNX,是「SET if Not eXists」的縮寫,也就是隻有不存在的時候才設定,可以利用它來實現鎖的效果。
2.資料預熱
可以通過快取reload機制,預先去更新快取,在即將發生大併發訪問前手動觸發載入快取不同的key,設定不同的過期時間,讓快取失效的時間點儘量均勻。
3.做二級快取
建立備份快取,快取A和快取B,A設定超時時間,B不設值超時時間,先從A讀快取,A沒有讀B,並且更新A快取和B快取;
4.快取永不過期
這裡的“永不過期“包含兩層意思
(1)從快取上看,缺失沒有設定過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
(2)從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value裡,如果發現要過期了,通過一個後臺的非同步執行緒進行快取的構建,也就是“邏輯”過期。
從實戰看,這種方法對於效能非常友好,唯一不足的就是構建快取時候,其餘執行緒(非構建快取的執行緒)可能訪問的是老資料,但是對於一般的網際網路功能來說這個還是可以忍受。
快取擊穿
概念
對於一些設定了過期時間的key,如果這些key可能會在某些時間點被超高併發地訪問,是一種非常“熱點”的資料。這個時候,需要考慮一個問題:快取被“擊穿”的問題,這個和快取雪崩的區別在於這裡針對某一key快取,前者則是很多key。
快取在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的併發請求過來,這些請求發現快取過期一般都會從後端DB載入資料並回設到快取,這個時候大併發的請求可能會瞬間把後端DB壓垮。
解決方案
和快取雪崩解決情況一樣
PHP+redis實現布隆過濾器
由於redis 是實現了setbit和getbit操作,天然是和實現布隆過濾器,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');
}複製程式碼