快取穿透 快取雪崩

Fysddsw_lc發表於2019-01-13

快取穿透

概念

快取穿透是指查詢一個一定不存在的資料,由於快取是不命中時需要從DB查詢,查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到資料庫去查詢,造成快取穿透。

解決方案

  1. 採用布隆過濾器,使用一個足夠大的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 是不會報告該元素存在於集合中的,所以不會漏報)。


  2. 訪問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');
}複製程式碼


相關文章