點擊上方"java全棧技術"關注,每天學習一個java知識點
前面文章講過
這次給大家整理一篇Redis經常被問到的問題:緩存穿透、緩存雪崩、緩存預熱、緩存更新、緩存降級等概念及簡單解決方案。
一、緩存穿透
緩存穿透是指用戶查詢數據庫沒有的數據,緩存中自然也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,然後返回空(相當於進行了兩次無用的查詢)。這樣請求繞過緩存直接查數據庫,緩存形同虛設,這也是經常提的緩存命中率問題。
有很多種方法可以有效地解決緩存穿透問題,最長見的有空對象和布隆過濾器兩種解決方案。
空對象是首選方案,簡單直接,碰到查詢結果為空的鍵,放一個空值在緩存中,下次再訪問就立刻知道這個鍵無效,不用發出SQL了。
但存在如下問題:
- 空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內存空間 ( 如果是攻擊,問題更嚴重 ),比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。
- 緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。例如過期時間設置為 5 分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消息系統或者其他方式清除掉緩存層中的空對象。
對於第一點,我還建議空值放在另外的緩存空間中,不宜與正常值共用空間,否則當空間不足時,緩存系統的LRU算法可能會先剔除正常值,再剔除空值——這個漏洞可能會受到攻擊。
對於第二點,如果是Redis緩存,更新數據後直接在Redis中清除即可;如果是本地緩存,就需要用消息來通知其他機器清除各自的本地緩存了。
布隆過濾器。將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率(錯誤率可調)和刪除困難。
二、緩存雪崩
可以理解為:由於原有緩存失效(過期),新緩存未到期間(如:採用了相同的緩存過期時間策略,在同一時刻出現大面積的緩存過期),所有原本應該訪問緩存的請求都去查詢數據庫,而對數據庫CPU和內存造成巨大壓力,甚至造成數據庫宕機,從而引起一系列連鎖反應,造成整個系統崩潰。
緩存正常從Redis中獲取,示意圖如下:
緩存失效瞬間示意圖如下:
解決方案
1:在緩存失效後,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
2:緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件
3:做二級緩存,A1為原始緩存,A2為拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置為短期,A2設置為長期
以下簡單介紹兩種實現方式的偽代碼:
(1)碰到這種情況,一般併發量不是特別多的時候,使用最多的解決方案是加鎖排隊,偽代碼如下:
加鎖排隊只是為了減輕數據庫的壓力,並沒有提高系統吞吐量。假設在高併發下,緩存重建期間key是鎖著的,這是過來1000個請求999個都在阻塞的。同樣會導致用戶等待超時,這是個治標不治本的方法!
注意:加鎖排隊的解決方式分佈式環境的併發問題,有可能還要解決分佈式鎖的問題;線程還會被阻塞,用戶體驗很差!因此,在真正的高併發場景下很少使用!
(2)還有一個解決方案是:給每一個緩存數據增加相應的緩存標記,記錄緩存的是否失效,如果緩存標記失效,則更新數據緩存,實例偽代碼如下:
解釋說明:
1、緩存標記:記錄緩存數據是否過期,如果過期會觸發通知另外的線程在後臺去更新實際key的緩存;
2、緩存數據:它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鐘,數據緩存設置為60分鐘。 這樣,當緩存標記key過期後,實際緩存還能把舊數據返回給調用端,直到另外的線程在後臺更新完成後,才會返回新緩存。
關於緩存崩潰的解決方法,這裡提出了三種方案:使用鎖或隊列、設置過期標誌更新緩存、為key設置不同的緩存失效時間,還有一個被稱為“二級緩存”的解決方法,有興趣的讀者可以自行研究。
三、緩存預熱
緩存預熱這個應該是一個比較常見的概念。新的緩存系統沒有任何緩存數據,在緩存重建數據的過程中,系統性能和數據庫負載都不太好,所以最好是在系統上線之前就把要緩存的熱點數據加載到緩存中,這種緩存預加載手段就是緩存預熱。
解決思路:
單機web系統情況下比較簡單。
- 直接寫個緩存刷新頁面,上線時手工操作下。
- 數據量不大,可以在WEB系統啟動的時候加載。
- 搞個定時器定時刷新緩存,或者由用戶觸發都行。
分佈式緩存系統,如Memcached,Redis,比如緩存系統比較大,由十幾臺甚至幾十臺機器組成,這樣預熱會複雜一些。
- 寫個程序去跑。
- 單個緩存預熱框架。
緩存預熱的目標就是在系統上線前,將數據加載到緩存中。
四、緩存更新
因為內存受限於空間緩存只能存儲有限的數據,因此我們需要決定在我們的應用場景中,使用何種緩存更新策略,下面介紹幾種常見的模式。
Cache-Aside模式
應用負責基於存儲讀寫數據,緩存不直接和存儲打交道,應用的行為如下:
- 檢索緩存,緩存沒有命中;
- 從數據庫加載數據;
- 將數據更新至緩存;
- 返回結果;
Memcached通常被應用於這種方式,這種模式對於接下來的數據讀取將非常快,Cache-Aside也叫做延遲加載,只有需要的數據被緩存,避免不需要的數據佔用緩存空間。
這種模式的缺點如下:
- 每次緩存沒命中都增加系統之間的交互,這將會增加響應延遲;
- 當對應數據庫中的數據被更新之後將出現髒數據問題。這個問題可以通過設置過期時間(TTL)來緩解,當時間過期將發生強制更新緩存;
- 當一個節點壞了之後,新的節點代替舊的節點,這個時候將出現大量的緩存穿透問題;
Write-Though模式
應用將緩存作為主要存儲,讀寫都直接和緩存打交道,緩存負責基於存儲進行讀寫:
- 應用基於緩存添加或刪除記錄;
- 緩存同步地將記錄寫入存儲;
- 返回;
Write-Though對於所有的寫操作都是比較慢的,但是對於讀來說很快,用戶通常需要容忍寫延遲,但是不會出現髒數據。
這種模式的缺點如下:
- 由於failure或者scaling帶來的新增節點的時候,新增節點在下次更新數據之前將沒有數據,這個問題可以結合Cache-Aside模式來緩解;
- 對於很多寫入的數據將永遠不會讀取到,這個問題可以通過設置過期時間解決;
Write-Behind模式
在這種模式下,應用的行為如:
- 直接讀寫緩存;
- 寫操作通知任務來異步進行更新;
這種模式的缺點如下:
- 如果在數據被更新到存儲之前緩存掛了,則數據將會丟失;
- 實現起來比Write-Though和Cache-Aside模式更為複雜;
Refresh-Ahead模式
我們可以配置緩存自動在最近訪問的數據過期之前更新它們,如果可以準確預測將要訪問的數據,Refresh-Ahead模式可以有效地減少讀寫的延遲。
這種模式的缺點如下:
- 如果預測數據不準確,則比不做什麼更有損性能;
五、緩存降級
當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,仍然需要保證服務還是可用的,即使是有損服務。系統可以根據一些關鍵數據進行自動降級,也可以配置開關實現人工降級。
降級的最終目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的(如加入購物車、結算)。
在進行降級之前要對系統進行梳理,看看系統是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日誌級別設置預案:
- 一般:比如有些服務偶爾因為網絡抖動或者服務正在上線而超時,可以自動降級;
- 警告:有些服務在一段時間內成功率有波動(如在95~100%之間),可以自動降級或人工降級,併發送告警;
- 錯誤:比如可用率低於90%,或者數據庫連接池被打爆了,或者訪問量突然猛增到系統能承受的最大閥值,此時可以根據情況自動降級或者人工降級;
- 嚴重錯誤:比如因為特殊原因數據錯誤了,此時需要緊急人工降級。
緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈,而降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峰或者問題解決後再打開。
六、總結
這些都是實際項目中可能碰到的一些問題,也是面試的時候經常會被問到的知識點,實際上還有很多各種各樣的問題,文中的解決方案,也不可能滿足所有的場景。一般正式的業務場景往往要複雜的多,應用場景不同,方法和解決方案也不同,具體解決方案要根據實際情況來確定!
閱讀更多 java全棧技術 的文章