緩存雪崩:由於原有的緩存過期失效,新的緩存還沒有緩存進來,有一只請求緩存請求不到,導致所有請求都跑去了數據庫,導致數據庫IO、內存和CPU眼里過大,甚至導致宕機,使得整個系統崩潰。
解決思路:
1,采用加鎖計數,或者使用合理的隊列數量來避免緩存失效時對數據庫造成太大的壓力。這種辦法雖然能緩解數據庫的壓力,但是同時又降低了系統的吞吐量。
2,分析用戶行為,盡量讓失效時間點均勻分布。避免緩存雪崩的出現。
3,如果是因為某台緩存服務器宕機,可以考慮做主備,比如:redis主備,但是雙緩存涉及到更新事務的問題,update可能讀到臟數據,需要好好解決。
加鎖:加鎖排隊只是為了減輕數據庫的壓力,並沒有提高系統吞吐量。假設在高並發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導致用戶等待超時,這是個治標不治本的方法。
public class CacheDemo
{
public object GetCacheDataList()
{
const int cacheTime = 60;
const string lockKey = cacheKey;
const string cacheKey = "datainfolist";
var cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null)
{
return cacheValue;
}
else
{
lock (lockKey)
{
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null)
{
return cacheValue;
}
else
{
cacheValue = GetDataBaseInfo();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
}
標記失效緩存:
緩存標記:記錄緩存數據是否過期,如果過期會觸發通知另外的線程在后台去更新實際key的緩存。
緩存數據:它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鍾,數據緩存設置為60分鍾。 這樣,當緩存標記key過期后,實際緩存還能把舊數據返回給調用端,直到另外的線程在后台更新完成后,才會返回新緩存。
這樣做后,就可以一定程度上提高系統吞吐量。
public object GetProductListNew()
{
const int cacheTime = 30;
const string cacheKey = "product_list";
//緩存標記。
const string cacheSign = cacheKey + "_sign";
var sign = CacheHelper.Get(cacheSign);
//獲取緩存值
var cacheValue = CacheHelper.Get(cacheKey);
if (sign != null)
{
return cacheValue; //未過期,直接返回。
}
else
{
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) =>
{
cacheValue = GetProductListFromDB(); //這里一般是 sql查詢數據。
CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期設緩存時間的2倍,用於臟讀。
});
return cacheValue;
}
}
緩存穿透:
緩存穿透是指用戶查詢數據,在數據庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,然后返回空。這樣請求就繞過緩存直接查數據庫,這也是經常提的緩存命中率問題。
解決的辦法就是:如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
緩存穿透是指用戶查詢數據,在數據庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫中查詢。
解決思路:
1,如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
2,根據緩存數據Key的規則。例如我們公司是做機頂盒的,緩存數據以Mac為Key,Mac是有規則,如果不符合規則就過濾掉,這樣可以過濾一部分查詢。在做緩存規划的時候,Key有一定規則的話,可以采取這種辦法。這種辦法只能緩解一部分的壓力,過濾和系統無關的查詢,但是無法根治。
3,采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的BitSet中,不存在的數據將會被攔截掉,從而避免了對底層存儲系統的查詢壓力。關於布隆過濾器,詳情查看:基於BitSet的布隆過濾器(Bloom Filter)
大並發的緩存穿透會導致緩存雪崩。
public object GetProductListNew()
{
const int cacheTime = 30;
const string cacheKey = "product_list";
var 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;
}
}
把空結果,也給緩存起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當查詢的值為空時引起的緩存穿透。同時也可以單獨設置個緩存區域存儲空值,對要查詢的key進行預先校驗,然后再放行給后面的正常緩存處理邏輯。
緩存預熱
緩存預熱就是系統上線后,將相關的緩存數據直接加載到緩存系統。這樣避免,用戶請求的時候,再去加載相關的數據。
解決思路:
1,直接寫個緩存刷新頁面,上線時手工操作下。
2,數據量不大,可以在WEB系統啟動的時候加載。
3,定時刷新緩存,
緩存更新
緩存淘汰的策略有兩種:
(1) 定時去清理過期的緩存。
(2)當有用戶請求過來時,再判斷這個請求所用到的緩存是否過期,過期的話就去底層系統得到新數據並更新緩存。
兩者各有優劣,第一種的缺點是維護大量緩存的key是比較麻煩的,第二種的缺點就是每次用戶請求過來都要判斷緩存失效,邏輯相對比較復雜,具體用哪種方案,大家可以根據自己的應用場景來權衡。1. 預估失效時間 2. 版本號(必須單調遞增,時間戳是最好的選擇)3. 提供手動清理緩存的接口。
