緩存雪崩 緩存雪崩是由於原有緩存失效(過期),新緩存未到期間。所有請求都去查詢資料庫,而對資料庫CPU和記憶體造成巨大壓力,嚴重的會造成資料庫宕機。從而形成一系列連鎖反應,造成整個系統崩潰。 解決方法: 一般併發量不是特別多的時候,使用最多的解決方案是加鎖排隊。 給每一個緩存數據增加相應的緩存標記,記 ...
緩存雪崩
緩存雪崩是由於原有緩存失效(過期),新緩存未到期間。所有請求都去查詢資料庫,而對資料庫CPU和記憶體造成巨大壓力,嚴重的會造成資料庫宕機。從而形成一系列連鎖反應,造成整個系統崩潰。
解決方法:
-
一般併發量不是特別多的時候,使用最多的解決方案是加鎖排隊。
-
給每一個緩存數據增加相應的緩存標記,記錄緩存的是否失效,如果緩存標記失效,則更新數據緩存。
- 緩存標記:記錄緩存數據是否過期,如果過期會觸發通知另外的線程在後臺去更新實際key的緩存。
- 緩存數據:它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鐘,數據緩存設置為60分鐘。 這樣,當緩存標記key過期後,實際緩存還能把舊數據返回給調用端,直到另外的線程在後臺更新完成後,才會返回新緩存。
加鎖排隊方案偽代碼:
//偽代碼 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //這裡一般是sql查詢數據 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } }
緩存標記方案偽代碼:
//偽代碼 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; } }
緩存穿透
緩存穿透是指用戶查詢數據,在資料庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去資料庫再查詢一遍,然後返回空(相當於進行了兩次無用的查詢)。這樣請求就繞過緩存直接查資料庫,這也是經常提的緩存命中率問題。
解決方案:
- 布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。
- 如果一個查詢返回的數據為空(不管是數據不存在,還是系統故障),我們仍然把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。通過這個直接設置的預設值存放到緩存,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問資料庫,這種辦法最簡單粗暴!
方案二偽代碼:
//偽代碼 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; } }
緩存預熱
緩存預熱這個應該是一個比較常見的概念,相信很多小伙伴都應該可以很容易的理解,緩存預熱就是系統上線後,將相關的緩存數據直接載入到緩存系統。這樣就可以避免在用戶請求的時候,先查詢資料庫,然後再將數據緩存的問題!用戶直接查詢事先被預熱的緩存數據!
解決思路:
- 直接寫個緩存刷新頁面,上線時手工操作下;
- 數據量不大,可以在項目啟動的時候自動進行載入;
- 定時刷新緩存;
緩存更新
除了緩存伺服器自帶的緩存失效策略之外,我們還可以根據具體的業務需求進行自定義的緩存淘汰,常見的策略有兩種:
- 定時去清理過期的緩存。
- 當有用戶請求過來時,再判斷這個請求所用到的緩存是否過期,過期的話就去底層系統得到新數據並更新緩存。
緩存降級
當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,仍然需要保證服務還是可用的,即使是有損服務。系統可以根據一些關鍵數據進行自動降級,也可以配置開關實現人工降級。
降級的最終目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的。
在進行降級之前要對系統進行梳理,看看系統是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日誌級別設置預案:
(1)一般:比如有些服務偶爾因為網路抖動或者服務正在上線而超時,可以自動降級;
(2)警告:有些服務在一段時間內成功率有波動(如在95~100%之間),可以自動降級或人工降級,併發送告警;
(3)錯誤:比如可用率低於90%,或者資料庫連接池被打爆了,或者訪問量突然猛增到系統能承受的最大閥值,此時可以根據情況自動降級或者人工降級;
(4)嚴重錯誤:比如因為特殊原因數據錯誤了,此時需要緊急人工降級。
------------------------推薦閱讀------------------------
2019年JVM最新面試題,必須收藏它
最全面的阿裡多線程面試題,你能回答幾個?
Java面試題:Java中的集合及其繼承關係
花了近十年的時間,整理出史上最全面Java面試題