天天看點

redis緩存穿透、擊穿、雪崩解決方案1.緩存穿透2.緩存擊穿3.緩存雪崩

1.緩存穿透

緩存穿透:key對應的資料在資料庫不存在,每次針對此key的請求從緩存擷取不到,請求都送到資料庫,進而可能壓垮資料庫。比如用一個使用者id擷取使用者資訊,一般情況是先從緩存中查詢,如果緩存沒有資料,那麼會往DB進行查詢,DB查詢出資料,則會将資料庫放入緩存,如果沒有就不會放入緩存,如果有人惡意用一個不可能存在的使用者id擷取資料,那麼可能壓垮資料庫。

##解決辦法:

  • 1.設定正則過濾。對于每一個緩存key都有一定的規範限制,這樣在程式中對不符合parttern的key的請求可以拒絕。
  • 2.使用bitmap(布隆過濾器)。将可能出現的緩存key的組合方式的所有數值以hash形式存儲在一個很大的bitmap中,一個一定不存在的資料會被這個bitmap攔截掉,進而避免了對底層存儲系統的查詢壓力。
本質上布隆過濾器是一種資料結構,比較巧妙的機率型資料結構(probabilistic data structure),特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”,在java中可以了解為一個hashset。
  • 3.将查詢空值放入redis(常用)。如果對應在資料庫中的資料都不存在,我們将此key對應的value設定為一個預設的值,比如“NULL”,并設定一個緩存的失效時間,避免類似的資料太多,對于redis造成一定壓力。這個key的時效比正常的時效要小的多,一般将過期時間設定在五分鐘以内。
/**
 * @description:避免緩存穿透的僞代碼
 * @param key 要查詢的key
 */
public String getValue(String key) {
	int cacheTime = 5 * 60 * 1000;//為空時候設定的過期時間
	String value = redisUtil.getKey(key);
	if (null != value) {//緩存不為空,則傳回
		return value;
	}else {
		//緩存為空,則向DB查詢
		value = curdUtil.getData();
		if(value == null) {
			value = "";
			redisUtil.set(key,value,cacheTime);//将為空的值放入并設定過期時間
			return value;
		}
		redisUtil.set(key,value);
	}
	return value;
}
           

2.緩存擊穿

緩存擊穿:熱點key在某個特殊的場景時間内恰好失效了(例如到期了),恰好有大量并發請求過來了,持續的大并發就穿破緩存,直接請求資料庫,就像在一個屏障上鑿開了一個洞。

##解決辦法:

  • 1.使用互斥鎖(mutex key),較常用。在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作傳回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作傳回成功時,再進行load db的操作并回設緩存;否則,就重試整個get緩存的方法。
SETNX,是「SET if Not eXists」的縮寫,也就是隻有不存在的時候才設定,可以利用它來實作鎖的效果。
/**
 * @description:避免緩存擊穿的僞代碼
 * @param key 要查詢的key
 */
public String getValue(String key) {
	String value = redisUtil.get(key);
	if(null == value) {//代表緩存過期了
		//設定3min的逾時,防止del操作失敗的時候,下次緩存過期一直不能load db
		String keyMutex = key+"_mutex";
		if (redisUtil.setnx(keyMutex, 1, 3 * 60) == 1) {  //代表設定成功
			value = curdUtil.get(key);
			redisUtil.set(key, value, expire_secs);
			redisUtil.del(key_mutex);
		}else {//這個時候代表同時候的其他線程已經load db并回設到緩存了,這時候重試擷取緩存值即可
			 sleep(50);
			 value = redisUtil.get(key);;  //重試
		}
	}
	return value;
}
           

3.緩存雪崩

緩存雪崩:與緩存擊穿的差別在于這裡針對很多key緩存,前者則是某一個key。

緩存失效時的雪崩效應對底層系統的沖擊非常可怕!大多數系統設計者考慮用加鎖或者隊列的方式保證來保證不會有大量的線程對資料庫一次性進行讀寫,進而避免失效時大量的并發請求落到底層存儲系統上。還有一個簡單方案就是将緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個随機值,比如1-5分鐘随機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。

1.随機值

/**
 * @description:避免緩存雪崩的随機值僞代碼
 * @param key
 */
public Object getValue(String key) {
	int cacheTime = 20;// 過期時間
	String cacheSign = key + "_sign";// 緩存标記
	String sign = redisUtil.get(cacheSign);
	// 擷取緩存值
	String value = redisUtil.get(key);
	if (null != sign) {
		return value; // 未過期,直接傳回
	} else {
		redisUtil.set(cacheSign, "1", cacheTime);
		ThreadPool.QueueUserWorkItem((arg) -> {
			// 向資料庫查詢
			value = curdUtil.get(key);
			// 日期設緩存時間的2倍,用于髒讀
			redisUtil.set(key, value, cacheTime * 2);
		});
	}
	return value;
}
           

解釋說明

  • 1.緩存标記:記錄緩存資料是否過期,如果過期會觸發通知另外的線程在背景去更新實際key的緩存;
  • 2.緩存資料:它的過期時間比緩存标記的時間延長1倍,例:标記緩存時間30分鐘,資料緩存設定為60分鐘。這樣,當緩存标記key過期後,實際緩存還能把舊資料傳回給調用端,直到另外的線程在背景更新完成後,才會傳回新緩存。

2.加鎖排隊

加鎖排隊隻是為了減輕資料庫的壓力,并沒有提高系統吞吐量。假設在高并發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導緻使用者等待逾時,這是個治标不治本的方法!
/**
 * @param key
 * @description:避免緩存雪崩的随機值僞代碼
 */
public Object getValue(String key) {
	int cacheTime = 20;// 過期時間
	String lockKey = key;
	String value = redisUtil.get(key);
	if(null != value) {
		return value;
	}else {
		synchronized (lockKey) {
			value = redisUtil.get(key); 
			if(null != value) {
				return value;
			}else {
				value = curdUtil.getData();
				redisUtil.set(key,value,cacheTime);
			}
		}
	}
	return value;
}
           

解釋說明

加鎖排隊隻是為了減輕資料庫的壓力,并沒有提高系統吞吐量。假設在高并發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導緻使用者等待逾時,這是個治标不治本的方法!

注意:加鎖排隊的解決方式分布式環境的并發問題,有可能還要解決分布式鎖的問題;線程還會被阻塞,使用者體驗很差!是以,在真正的高并發場景下很少使用!

3.二級緩存

待調研