緩存穿透、雪崩、熱點與Redis

(拼多多問:Redis雪崩解決辦法)html

導讀:互聯網系統中不可避免要大量用到緩存,在緩存的使用過程當中,架構師須要注意哪些問題?本文以 Redis 爲例,詳細探討了最關鍵的 3 個問題。git

 

1、緩存穿透預防及優化github

 

緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,可是出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層,如圖 11-3 所示整個過程分爲以下 3 步:redis

 

  1. 緩存層不命中算法

  2. 存儲層不命中,因此不將空結果寫回緩存數據庫

  3. 返回空結果後端

 

緩存穿透將致使不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義。緩存

 

緩存穿透模型架構

 

緩存穿透問題可能會使後端存儲負載加大,因爲不少後端存儲不具有高併發性,甚至可能形成後端存儲宕掉。一般能夠在程序中分別統計總調用數、緩存層命中數、存儲層命中數,若是發現大量存儲層空命中,可能就是出現了緩存穿透問題。併發

 

形成緩存穿透的基本有兩個。第一,業務自身代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等形成大量空命中,下面咱們來看一下如何解決緩存穿透問題。

 

緩存穿透的解決方法

 

1)緩存空對象

 

以下圖所示,當第 2 步存儲層不命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取,保護了後端數據源。

 

緩存空值應對穿透問題

 

緩存空對象會有兩個問題:

 

第一,空值作了緩存,意味着緩存層中存了更多的鍵,須要更多的內存空間 ( 若是是攻擊,問題更嚴重 ),比較有效的方法是針對這類數據設置一個較短的過時時間,讓其自動剔除。

 

第二,緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有必定影響。例如過時時間設置爲 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

 

使用布隆過濾器應對穿透問題

 

這種方法適用於數據命中不高,數據相對固定實時性低(一般是數據集較大)的應用場景,代碼維護較爲複雜,可是緩存空間佔用少。

 

兩種方案對比

 

前面介紹了緩存穿透問題的兩種解決方法 ( 實際上這個問題是一個開放問題,有不少解決方法 ),下面經過下表從適用場景和維護成本兩個方面對兩種方案進行分析。

 

緩存空對象和布隆過濾器方案對比

 

2、緩存雪崩問題優化

 

從下圖能夠很清晰出什麼是緩存雪崩:因爲緩存層承載着大量請求,有效的保護了存儲層,可是若是緩存層因爲某些緣由總體不能提供服務,因而全部的請求都會達到存儲層,存儲層的調用量會暴增,形成存儲層也會掛掉的狀況 

緩存層不可用引發的雪崩

 

雪崩的概念能夠簡單描述爲:緩存因爲某些緣由形成大量的緩存數據失效,大量的訪問請求直接打到數據庫或者服務接口,形成底層數據源的壓力。

有一種常見狀況的雪崩,就是在短期內大量的同步數據到緩存,到了過時時間,致使大量的緩存數據失效,從而造成雪崩現象。

解決方法:

1.在緩存失效後,經過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。好比對某個key只容許一個線程查詢數據和寫緩存,其餘線程等待。(與下面的熱點Key問題類似)

2.能夠經過緩存reload機制,預先去更新緩存,再即將發生大併發訪問前手動觸發加載緩存

3.不一樣的key,設置不一樣的過時時間,讓緩存失效的時間點儘可能均勻

4.作二級緩存,或者雙緩存策略。A1爲原始緩存,A2爲拷貝緩存,A1失效時,能夠訪問A2,A1緩存失效時間設置爲短時間,A2設置爲長期。

 

3、緩存熱點 key 重建優化

 

開發人員使用緩存 + 過時時間的策略既能夠加速數據讀寫,又保證數據的按期更新,這種模式基本可以知足絕大部分需求。可是有兩個問題若是同時出現,可能就會對應用形成致命的危害:

 

  • 當前 key 是一個熱點 key( 例如一個熱門的娛樂新聞),併發量很是大。

  • 重建緩存不能在短期完成,多是一個複雜計算,例如複雜的 SQL、屢次 IO、多個依賴等。

 

在緩存失效的瞬間,有大量線程來重建緩存 ( 以下圖),形成後端負載加大,甚至可能會讓應用崩潰。

 

熱點 key 失效後大量線程重建緩存

 

要解決這個問題也不是很複雜,可是不能爲了解決這個問題給系統帶來更多的麻煩,因此須要制定以下目標:

 

  • 減小重建緩存的次數

  • 數據儘量一致

  • 較少的潛在危險

 

1)互斥鎖 (mutex key)

 

此方法只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完,從新從緩存獲取數據便可,整個過程如圖 11-17。

 

使用互斥鎖重建緩存

 

下面代碼使用 Redis 的 setnx 命令實現上述功能。

SETNX key value

將 key 的值設爲 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不作任何動做。

SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。

 返回值:

設置成功,返回  1 。
設置失敗,返回  0 。

 

(1) 從 Redis 獲取數據,若是值不爲空,則直接返回值,不然執行 (2.1) 和 (2.2)。

 

(2) 若是 set(nx 和 ex) 結果爲 true,說明此時沒有其餘線程重建緩存,那麼當前線程執行緩存構建邏輯。

 

(2.2) 若是 setnx(nx 和 ex) 結果爲 false,說明此時已經有其餘線程正在執行構建緩存的工做,那麼當前線程將休息指定時間 ( 例如這裏是 50 毫秒,取決於構建緩存的速度 ) 後,從新執行函數,直到獲取到數據。

2)永遠不過時

 

「永遠不過時」包含兩層意思:

 

  • 從緩存層面來看,確實沒有設置過時時間,因此不會出現熱點 key 過時後產生的問題,也就是「物理」不過時

  • 從功能層面來看,爲每一個 value 設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。

 

整個過程以下圖所示:

 

" 永遠不過時 " 策略

 

從實戰看,此方法有效杜絕了熱點 key 產生的問題,但惟一不足的就是重構緩存期間,會出現數據不一致的狀況,這取決於應用方是否容忍這種不一致。下面代碼使用 Redis 進行模擬:

 

 

做爲一個併發量較大的應用,在使用緩存時有三個目標:第一,加快用戶訪問速度,提升用戶體驗。第二,下降後端負載,減小潛在的風險,保證系統平穩。第三,保證數據「儘量」及時更新。下面將按照這三個維度對上述兩種解決方案進行分析。

 

  • 互斥鎖 (mutex key):這種方案思路比較簡單,可是存在必定的隱患,若是構建緩存過程出現問題或者時間較長,可能會存在死鎖和線程池阻塞的風險,可是這種方法可以較好的下降後端存儲負載並在一致性上作的比較好。

  • " 永遠不過時 ":這種方案因爲沒有設置真正的過時時間,實際上已經不存在熱點 key 產生的一系列危害,可是會存在數據不一致的狀況,同時代碼複雜度會增大。

 

兩種解決方法對好比下表所示。

 

兩種熱點 key 的解決方法

本文列舉了緩存設計中最關鍵的 3 個問題,節選自機械工業出版社《Redis開發與運維》第 11 章。

 

https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug?

相關文章
相關標籤/搜索