1 緩存雪崩(Cache Avalanche)
1.1 什麼是緩存雪崩?
由于
- 應用設計層面,大量Key同時過期
- 緩存服務當機
等原因,可能導緻緩存資料同一時刻大規模不可用,或者都更新。
集中過期,其實不是太緻命,最緻命的是緩存伺服器某個節點當機:
- 自然形成的緩存雪崩,一定是在某個時間段集中建立緩存,那麼這時DB也可頂住壓力,無非就是對DB産生周期性壓力
- 而緩存服務節點的當機,這時所有緩存 key 都沒了,請求全部打入 DB,對DB造成的壓力不可預知,很可能瞬間就把DB壓垮,需通過主從叢集哨兵等解決
像電商項目,一般采取将不同分類的商品,緩存不同周期。在同一分類中的商品,加上一個随機因子。盡可能分散緩存過期時間,而且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源。
1.2 解決方案
- 更新政策在時間上做到比較均勻
- 使用的熱資料盡量分散到不同的機器上
- 多台機器做主從複制或多副本,實作高可用
1.2.1 差異化緩存過期時間
不要讓大量Key同時過期。
在原有失效時間基礎上增加一個随機值,比如1~5分鐘的随機,這樣每個緩存的過期時間重複率就會降低,集體失效機率也會大大降低。
1.2.2 讓緩存不主動過期
初始化緩存資料的時候設定緩存永不過期,然後啟動一個背景線程30秒一次定時把所有資料更新到緩存,而且通過适當休眠,控制從DB更新資料的頻率,降低DB壓力。
兩種解決方案截然不同,若無法全量緩存所有資料,則隻能使用方案一。
即使使用了方案二,緩存永不過期,同樣需在查詢時,確定有回源的邏輯。因為我們無法確定緩存系統中的資料永不丢失。
不管哪個方案,在把資料從DB加入緩存時,都需判斷來自DB的資料是否合法,比如最基本的判空!不然在某個時間點,如果DBA把 DB原始資料歸檔了。
因為緩存中的資料一直在是以一開始沒什麼問題,但也許N年後,某天緩存資料突然過期了,就從DB查到空資料加入緩存!
2 緩存穿透(Cache Penetration)
2.1 什麼是緩存穿透?
高并發查詢不存在的key,導緻将壓力都直接透傳到DB。
-
為何會多次透傳?
因為緩存不存在該資料,一直為空。
注意讓緩存能夠區分 key 是不存在 or 存在但查詢得到一個空值。
例如:通路id=-1的資料。可能出現繞過Redis頻繁通路DB,稱為緩存穿透,多出現在查詢為null的情況不被緩存時。
2.2 解決方案
布隆過濾器 or RoaringBitmap
提供一個能迅速判斷請求是否有效的攔截機制。
比如利用布隆過濾器,維護一系列合法有效的 key。進而能迅速判斷出,請求所攜帶的 Key 是否合法有效:
- 若不合法,則直接傳回,避免直接查詢DB。
緩存空值key
如果從DB查詢的對象為空,也放入緩存,隻是設定的緩存過期時間較短,比如設定為 60 s。
這樣第一次不存在也會被加載會記錄,下次拿到有這個key。
完全以緩存為準
更簡單粗暴的,若一個查詢傳回的資料為空,不管是:
- 資料不存在
- 還是系統故障
仍緩存該空結果,但其過期時間很短,最長不超過5min。
仍緩存該空結果,但其過期時間很短,最長不超過5min。
異步更新
使用 延遲異步加載 的政策2,這樣業務前端不會觸發更新,隻有我們資料更新時後端去主動更新。
服務降級
hystrix
互斥鎖(不推薦)
問題根本在于限制處理線程的數量,即key的更新操作添加全局互斥鎖。
在緩存失效時(判斷拿出來的值為空),不是立即去load db,而是
- 先使用緩存工具的某些帶成功操作傳回值的操作(Redis的SETNX)去set一個mutex key
- 當操作傳回成功時,再load db的操作并回設緩存;否則,就重試整個get緩存的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { // 緩存已過期
// 設定逾時,防止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;
}
}
提前"使用互斥鎖(不推薦)
在value内部設定1個逾時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1并重新設定到cache。然後再從資料庫加載資料并設定到cache中。僞代碼如下:
v = memcache.get(key);
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);
// load the latest value from db
v = db.get(key);
v.timeout = KEY_TIMEOUT;
memcache.set(key, value, KEY_TIMEOUT * 2);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
}