前面講過一些redis 緩存的使用和數據持久化。感興趣的朋友可以看看之前的文章,http://www.cnblogs.com/zhangweizhong/category/771056.html 。今天總結總結緩存使用過程中遇到的一些常見的問題。比如緩存雪崩,緩存穿透,緩存預熱等等。
緩存雪崩
緩存雪崩是由於原有緩存失效(過期),新緩存未到期間。所有請求都去查詢數據庫,而對數據庫CPU和內存造成巨大壓力,嚴重的會造成數據庫宕機。從而形成一系列連鎖反應,造成整個系統崩潰。
1. 碰到這種情況,一般並發量不是特別多的時候,使用最多的解決方案是加鎖排隊。
public object GetProductListNew() { const int cacheTime = 30; const string cacheKey = "product_list"; const string lockKey = cacheKey; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { lock (lockKey) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { cacheValue = GetProductListFromDB(); //這里一般是 sql查詢數據。 CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } }
2. 加鎖排隊只是為了減輕數據庫的壓力,並沒有提高系統吞吐量。假設在高並發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導致用戶等待超時,這是個治標不治本的方法。
還有一個解決辦法解決方案是:給每一個緩存數據增加相應的緩存標記,記錄緩存的是否失效,如果緩存標記失效,則更新數據緩存。
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; } }
緩存標記:記錄緩存數據是否過期,如果過期會觸發通知另外的線程在后台去更新實際key的緩存。
緩存數據:它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鍾,數據緩存設置為60分鍾。 這樣,當緩存標記key過期后,實際緩存還能把舊數據返回給調用端,直到另外的線程在后台更新完成后,才會返回新緩存。
這樣做后,就可以一定程度上提高系統吞吐量。
緩存穿透
緩存穿透是指用戶查詢數據,在數據庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,然后返回空。這樣請求就繞過緩存直接查數據庫,這也是經常提的緩存命中率問題。
解決的辦法就是:如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
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. 提供手動清理緩存的接口。
我前面有篇文章,是介紹緩存系統的緩存更新的。感興趣的朋友可以看看:http://www.cnblogs.com/zhangweizhong/p/5884761.html
總結
這些都是實際項目中,可能碰到的一些問題。實際上還有很多很多各種各樣的問題。緩存層框架的封裝往往要復雜的多。應用場景不同,方法和解決方案也不同。具體要根據實際情況來取舍。