今天又學到了很多,感覺雪崩和穿透很有意思理解起來也比較清晰,然後我搜索了一些資料,給自己做一個普及
我們通常使用 緩存 + 過期時間的策略來幫助我們加速接口的訪問速度,減少了後端負載,同時保證功能的更新
緩存穿透
緩存系統,按照KEY去查詢VALUE,當KEY對應的VALUE一定不存在的時候並對KEY併發請求量很大的時候,就會對後端造成很大的壓力。
(查詢一個必然不存在的數據。比如文章表,查詢一個不存在的id,每次都會訪問DB,如果有人惡意破壞,很可能直接對DB造成影響。)
由於緩存不命中,每次都要查詢持久層。從而失去緩存的意義。
解決方法:
1、緩存層緩存空值。
–緩存太多空值,佔用更多空間。(優化:給個空值過期時間)
–存儲層更新代碼了,緩存層還是空值。(優化:後臺設置時主動刪除空值,並緩存把值進去)
2、將數據庫中所有的查詢條件,放到布隆過濾器中。當一個查詢請求來臨的時候,先經過布隆過濾器進行檢查,如果請求存在這個條件中,那麼繼續執行,如果不在,直接丟棄。
備註:
比如數據庫中有10000個條件,那麼布隆過濾器的容量size設置的要稍微比10000大一些,比如12000.
對於誤判率的設置,根據實際項目,以及硬件設施來具體決定。但是一定不能設置爲0,並且誤判率設置的越小,哈希函數跟數組長度都會更多跟更長,那麼對硬件,內存中間的要求就會相應的高。
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.0001);
有了size跟誤判率,那麼布隆過濾器就會產生相應的哈希函數跟數組。
綜上:我們可以利用布隆過濾器,將redis緩存擊穿控制在一個可容忍的範圍內。
布隆過濾器:
BloomFiler又叫布隆過濾器,下面舉例說明BlooFilter的實現原理:
比如你有10個Url,你完全可以創建一長度是100bit的數組,然後對url分別用5個不同的hash函數進行hash,得到5個hash後的值,這5個值儘可能的保證均勻分佈在100個bit的範圍內。然後把5個hash值對應的bit位都置爲1,判斷一個url是否已經存在時,一次看5個bit位是否爲1就可以了,如果有任何一個不爲1,那麼說明這個url不存在。這裏需要注意的是,如果對應的bit位值都爲1,那麼也不能肯定這個url一定存在,
當布隆過濾器中位圖的 true
個數越來越多時,誤判會越來越嚴重,因此,當不知道數據規模時,應該使用會自動擴容的布隆過濾器。
緩存雪崩(緩存失效)
如果緩存集中在一段時間內失效,發生大量的緩存穿透,所有的查詢都落在數據庫上,造成了緩存雪崩。
緩存層宕掉後,流量會像奔逃的野牛一樣,打向後端存儲
解決方法:
- 在緩存失效後,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
- 可以通過緩存reload機制,預先去更新緩存,在即將發生大併發訪問前手動觸發加載緩存
- 不同的key,設置不同的過期時間,讓緩存失效的時間點儘量均勻
- 做二級緩存,或者雙緩存策略。A1爲原始緩存,A2爲拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置爲短期,A2設置爲長期。
熱點key
(1) 這個key是一個熱點key(例如一個重要的新聞,一個熱門的八卦新聞等等),所以這種key訪問量可能非常大。
(2) 緩存的構建是需要一定時間的。(可能是一個複雜計算,例如複雜的sql、多次IO、多個依賴(各種接口)等等)
於是就會出現一個致命問題:在緩存失效的瞬間,有大量線程來構建緩存(見下圖),造成後端負載加大,甚至可能會讓系統崩潰 。
解決方法:
1. 使用互斥鎖(mutex key):這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其他線程等待構建緩存的線程執行完,重新從緩存獲取數據就可以了
2. "提前"使用互斥鎖(mutex key):在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設置到cache。然後再從數據庫加載數據並設置到cache中。
3. "永遠不過期":
這裏的“永遠不過期”包含兩層意思:
(1) 從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
(2) 從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value裏,如果發現要過期了,通過一個後臺的異步線程進行緩存的構建,也就是“邏輯”過期
4. 資源保護:可以做資源的隔離保護主線程池,如果把這個應用到緩存的構建也未嘗不可。
四種方案對比:
作爲一個併發量較大的互聯網應用,我們的目標有3個:
1. 加快用戶訪問速度,提高用戶體驗。
2. 降低後端負載,保證系統平穩。
3. 保證數據“儘可能”及時更新(要不要完全一致,取決於業務,而不是技術。)
所以第二節中提到的四種方法,可以做如下比較,還是那句話:沒有最好,只有最合適。
解決方案 | 優點 | 缺點 |
簡單分佈式鎖(Tim yang) |
1. 思路簡單 2. 保證一致性 |
1. 代碼複雜度增大 2. 存在死鎖的風險 3. 存在線程池阻塞的風險 |
加另外一個過期時間(Tim yang) | 1. 保證一致性 | 同上 |
不過期(本文) |
1. 異步構建緩存,不會阻塞線程池 |
1. 不保證一致性。 2. 代碼複雜度增大(每個value都要維護一個timekey)。 3. 佔用一定的內存空間(每個value都要維護一個timekey)。 |
資源隔離組件hystrix(本文) |
1. hystrix技術成熟,有效保證後端。 2. hystrix監控強大。
|
1. 部分訪問存在降級策略。 |
總結
1. 熱點key + 過期時間 + 複雜的構建緩存過程 => mutex key問題
2. 構建緩存一個線程做就可以了。
3. 四種解決方案:沒有最佳只有最合適。