項目中業務數據基本上都是存在關系型數據庫中,如:mysql,oracle,sqlServer等數據庫,項目上線初期,由於用戶規模還比較小,系統訪問量不大。關系性數據庫可以抗住並發較小的請求。隨着業務的增長用戶的增加系統整體的並發請求增大。關系型數據庫處理能力跟不上,在對數據庫做主從讀寫分離,分布式設計之前,引入緩存可以有效提高系統整體的並發。如:redis非關系型數據庫。但是在使用緩存redis的時候也存在相應的問題:緩存穿透,擊穿,雪崩,如果不注意會導致並發壓力繞過緩存直接落在數據庫上導致數據庫阻塞,崩潰等問題。
1.redis緩存穿透,擊穿,雪崩概念
-
緩存穿透:
key對應的數據在數據源並不存在,每次針對此key的請求從緩存獲取不到,請求都會到數據源,從而可能壓垮數據源。比如用一個不存在的用戶id獲取用戶信息,不論緩存還是數據庫都沒有,若黑客利用此漏洞進行攻擊可能壓垮數據庫。 -
緩存擊穿:
key對應的數據存在,但在redis中過期,此時若有大量並發請求過來,這些請求發現緩存過期一般都會從后端DB加載數據並回設到緩存,這個時候大並發的請求可能會瞬間把后端DB壓垮。 -
緩存雪崩:
當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(比如DB)帶來很大壓力。
2.緩存穿透
故障描述:
緩存與數據庫都沒有的數據,發起大量訪問請求,對后端造成很大的壓力。要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
解決方案:
1. 緩存層緩存空值
-緩存太多空值,占用更多空間。(優化:給個空值過期時間)
-存儲層更新代碼了,緩存層還是空值。(優化:后台設置時主動刪除空值,並緩存把值進去)
//偽代碼
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
}
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//數據庫查詢不到,為空
cacheValue = GetProductListFromDB();
if (cacheValue == null) {
//如果發現為空,設置個默認值,也緩存起來
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
2. 布隆過濾器
-將數據庫中所有的查詢條件,放到布隆過濾器中。當一個查詢請求來臨的時候,先經過布隆過濾器進行檢查,如果請求存在這個條件中,那么繼續執行,如果不在,直接丟棄。
3. 接口參數驗簽
過濾掉特殊值,比如id= -1
3.緩存擊穿
故障描述:熱點單個key,過期,此時迎來高並發
對於一些設置了過期時間的key,如果這些key可能會在某些時間點被超高並發地訪問,是一種非常“熱點”的數據。這個時候,需要考慮一個問題:緩存被“擊穿”的問題,這個和緩存雪崩的區別在於這里針對某一key緩存,前者則是很多key。
場景一:微博上,某某明星傳緋聞,兩個明星主頁被刷爆
緩存在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的並發請求過來,這些請求發現緩存過期一般都會從后端DB加載數據並回設到緩存,這個時候大並發的請求可能會瞬間把后端DB壓垮。
解決方案:
- 使用互斥鎖(mutex key)
業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作並回設緩存;否則,就重試整個get緩存的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表緩存值過期
//設置3min的超時,防止del操作失敗的時候,下次緩存過期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表設置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //這個時候代表同時候的其他線程已經load db並回設到緩存了,這時候重試獲取緩存值即可
sleep(50);
get(key); //重試
}
}
else {
return value;
}
}
-
"提前"使用互斥鎖(mutex key):
在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設置到cache。然后再從數據庫加載數據並設置到cache中。 -
"永遠不過期":這里的“永遠不過期”包含兩層意思:
從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value里,如果發現要過期了,通過一個后台的異步線程進行緩存的構建,也就是“邏輯”過期。 -
資源保護:
采用netflix的hystrix,可以做資源的隔離保護主線程池,如果把這個應用到緩存的構建也未嘗不可。 -
為即將過期的key,續命:
緩存中取值,發現即將過期,追加一個小的時間值,延長有效期。 -
限流:
比如說使用消息隊列,讓流量在消息隊列中囤積下,逐個消費,緩解后端壓力。
4.緩存雪崩
故障描述:緩存雪崩是指在我們設置緩存時采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩。(與緩存擊穿的區別在於這里針對很多key緩存,前者則是某一個key)
場景一:同一時間點,緩存全部失效:
比如:有的電商網站在項目重啟時,將全部商品信息加入緩存中,並設置等長的有效期。到某一時間點,緩存有效期過期,大量緩存失效,如果遇上高並發,請求全部打在后端數據庫上。
場景二:穿透誘發雪崩:
短時間內大量的請求無法命中緩存,請求穿透到數據庫,導致數據庫繁忙,請求超時。大量的請求超時還會引發更多的重試請求,更多的重試請求讓數據庫更加繁忙,這樣惡性循環導致系統雪崩。
解決方案:
-
設置緩存超時時間的時候加上一個隨機的時間長度:
比如這個緩存key的超時時間是固定的5分鍾加上隨機的2分鍾,醬紫可從一定程度上避免雪崩問題。 -
熱點數據永不過期,數據庫更新時同步更新緩存 (如:電商首頁信息)
-
互斥鎖排隊
根據key獲取value值為空時,鎖上,從數據庫中load數據后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時間后重試。這里要注意,分布式環境中要使用分布式鎖(Redisson),單機的話用普通的鎖(synchronized、Lock)。 -
設置過期標志更新緩存
//偽代碼
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
//緩存標記
String cacheSign = cacheKey + "_sign";
String sign = CacheHelper.Get(cacheSign);
//獲取緩存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
return cacheValue; //未過期,直接返回
} else {
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) -> {
//這里一般是 sql查詢數據
cacheValue = GetProductListFromDB();
//日期設緩存時間的2倍,用於臟讀
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
參考:https://www.jianshu.com/p/44d4bc0a9f10
參考:https://www.cnblogs.com/xichji/p/11286443.html