使用緩存時有三個目標:前端
第一,加快用戶訪問速度,提升用戶體驗git
第二,下降後端負載,減小潛在的風險,保證系統平穩github
第三,保證數據「儘量」及時更新算法
緩存穿透緣由
緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,可是出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層後端
緩存層不命中緩存
存儲層不命中,因此不將空結果寫回緩存微信
返回空結果架構
緩存穿透將致使不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義。併發
緩存穿透問題可能會使後端存儲負載加大,因爲不少後端存儲不具有高併發性,甚至可能形成後端存儲宕掉。一般能夠在程序中分別統計總調用數、緩存層命中數、存儲層命中數,若是發現大量存儲層空命中,可能就是出現了緩存穿透問題。編輯器
形成緩存穿透的基本有兩個:
業務自身代碼或者數據出現問題
一些惡意攻擊、爬蟲等形成大量空命中
緩存穿透的解決方法
1)緩存空對象
當存儲層不命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取,保護了後端數據源。
緩存空對象會有兩個問題:
空值作了緩存,意味着緩存層中存了更多的鍵,須要更多的內存空間 ( 若是是攻擊,問題更嚴重 ),比較有效的方法是針對這類數據設置一個較短的過時時間,讓其自動剔除。
緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有必定影響。例如過時時間設置爲 5 分鐘,若是此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時能夠利用消息系統或者其餘方式清除掉緩存層中的空對象。
2)布隆過濾器攔截
在訪問緩存層和存儲層以前,將存在的 key 用布隆過濾器提早保存起來,作第一層攔截。例如: 一個個性化推薦系統有 4 億個用戶 ID,每一個小時算法工程師會根據每一個用戶以前歷史行爲作出來的個性化放到存儲層中,可是最新的用戶因爲沒有歷史行爲,就會發生緩存穿透的行爲,爲此能夠將全部有個性化推薦數據的用戶作成布隆過濾器。若是布隆過濾器認爲該用戶 ID 不存在,那麼就不會訪問存儲層,在必定程度保護了存儲層。
有關布隆過濾器的相關知識,能夠參考: https://en.wikipedia.org/wiki/Bloom_filter
能夠利用 Redis 的 Bitmaps 實現布隆過濾器,GitHub 上已經開源了相似的方案,讀者能夠進行參考:https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter
這種方法適用於數據命中不高,數據相對固定實時性低(一般是數據集較大)的應用場景,代碼維護較爲複雜,可是緩存空間佔用少。
緩存雪崩問題優化
預防和解決緩存雪崩問題,能夠從如下三個方面進行着手。
1)保證緩存層服務高可用性。
和飛機都有多個引擎同樣,若是緩存層設計成高可用的,即便個別節點、個別機器、甚至是機房宕掉,依然能夠提供服務
2)依賴隔離組件爲後端限流並降級。
不管是緩存層仍是存儲層都會有出錯的機率,能夠將它們視同爲資源。做爲併發量較大的系統,假若有一個資源不可用,可能會形成線程所有 hang 在這個資源上,形成整個系統不可用。降級在高併發系統中是很是正常的:好比推薦服務中,若是個性化推薦服務不可用,能夠降級補充熱點數據,不至於形成前端頁面是開天窗。
在實際項目中,咱們須要對重要的資源 ( 例如 Redis、 MySQL、 Hbase、外部接口 ) 都進行隔離,讓每種資源都單獨運行在本身的線程池中,即便個別資源出現了問題,對其餘服務沒有影響。可是線程池如何管理,好比如何關閉資源池,開啓資源池,資源池閥值管理,這些作起來仍是至關複雜的,這裏推薦一個 Java 依賴隔離工具 Hystrix(https://github.com/Netflix/Hystrix)
3)提早演練。在項目上線前,演練緩存層宕掉後,應用以及後端的負載狀況以及可能出現的問題,在此基礎上作一些預案設定。
緩存熱點 key 重建優化
開發人員使用緩存 + 過時時間的策略既能夠加速數據讀寫,又保證數據的按期更新,這種模式基本可以知足絕大部分需求。可是有兩個問題若是同時出現,可能就會對應用形成致命的危害:
當前 key 是一個熱點 key( 例如一個熱門的娛樂新聞),併發量很是大。
重建緩存不能在短期完成,多是一個複雜計算,例如複雜的 SQL、屢次 IO、多個依賴等。
在緩存失效的瞬間,有大量線程來重建緩存,形成後端負載加大,甚至可能會讓應用崩潰。
解決思路:
1)互斥鎖 (mutex key)
只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完,從新從緩存獲取數據便可
2)永遠不過時,「永遠不過時」包含兩層意思:
從緩存層面來看,確實沒有設置過時時間,因此不會出現熱點 key 過時後產生的問題,也就是「物理」不過時。
從功能層面來看,爲每一個 value 設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。
方案比較:
互斥鎖 (mutex key):這種方案思路比較簡單,可是存在必定的隱患,若是構建緩存過程出現問題或者時間較長,可能會存在死鎖和線程池阻塞的風險,可是這種方法可以較好的下降後端存儲負載並在一致性上作的比較好。
" 永遠不過時 ":這種方案因爲沒有設置真正的過時時間,實際上已經不存在熱點 key 產生的一系列危害,可是會存在數據不一致的狀況,同時代碼複雜度會增大。
本文分享自微信公衆號 - JAVA高級架構(gaojijiagou)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。