?前言:此篇文章系本人學習過程中記錄下來的筆記,里面難免會有不少欠缺的地方,誠心期待大家多多給予指教。
基礎篇:
- Redis(一)
- Redis(二)
- Redis(三)
- Redis(四)
- Redis(五)
- Redis(六)
- Redis(七)
- Redis(八)
進階篇:
- Redis(九)
- Redis(十)
- Redis(十一)
- Redis(十二)
接上期內容:上期完成了相關案例的學習。下面學習緩存穿透、預熱、雪崩、擊穿,話不多說,直接發車。
一、緩存預熱
(一)、定義
緩存預熱是一種在系統啟動階段或者特定時間點,將一些經常訪問或者關鍵的數據提前加載到緩存中的操作,以減少對數據源(如數據庫)的訪問次數,從而提高系統的響應速度和性能。避免在用戶首次請求時才去加載數據而導致的性能延遲。
(二)、功能
- 減少首次請求延遲:當用戶首次訪問某些數據時,如果沒有進行緩存預熱,系統需要從數據庫等數據源中查詢數據,這個過程可能會比較耗時。
- 減輕數據庫壓力:在系統運行初期,如果大量用戶同時發起請求,這些請求都直接訪問數據庫,會給數據庫帶來巨大的壓力,甚至可能導致數據庫性能下降或者崩潰。緩存預熱可以將部分數據提前加載到緩存中,后續的請求優先從緩存中獲取數據,從而減少了對數據庫的訪問頻率,降低了數據庫的負載
- 提高系統性能和穩定性:由于緩存的讀寫速度通常比數據庫等數據源快很多,緩存預熱可以讓系統在處理請求時更快地獲取數據,從而提高系統的整體性能。同時,減少了對數據庫的依賴,降低了因數據庫故障或性能問題導致系統不可用的風險,增強了系統的穩定性。
(三)、常用方案
- ?硬編碼(不大推薦):在代碼中直接明確地指定需要加載到緩存的數據和邏輯,不通過外部配置或動態計算來改變。在系統啟動時,按照預先編寫好的代碼邏輯將數據加載到緩存中。
- @PostConstruct注解:?Java 中的一個注解,用于標記一個方法,該方法會在依賴注入完成之后、對象正式投入使用之前被自動調用。
- 定時器任務:通過定時任務框架(如 Spring 的@Scheduled注解、Quartz 等),按照預設的時間間隔(如每天凌晨、每小時等)自動執行緩存預熱操作。
- 數據腳本:使用腳本語言(如 Python、Shell 等)編寫腳本,通過腳本連接到緩存系統,將數據加載到緩存中。?
二、緩存雪崩
(一)、名詞解釋
緩存雪崩是指在某一時刻,緩存中大量的鍵在同一時間點或者在極短的時間內集中過期失效,或者緩存服務器發生故障導致緩存服務不可用,此時大量原本可以從緩存中獲取數據的請求,都直接涌向了數據庫等后端數據源,給數據庫帶來巨大的壓力,甚至可能導致數據庫不堪重負而崩潰,進而使整個系統出現性能急劇下降、服務不可用等嚴重問題。
(二)、發生場景
1、硬件方面
緩存服務器的硬件設備(如硬盤、內存、網卡等)出現故障,可能會導致緩存服務無法正常運行,從而發生緩存雪崩現象。
2、業務方面
大量的業務key同時過期,比如在進行緩存預熱時,為大量緩存數據設置了相同的過期時間,當這個過期時間到達時,這些緩存數據會同時失效,從而引起緩存雪崩現象。
(三)、預防與解決措施
硬件方面無法把控,主要從業務方面來解決。
業務方面:
-
避免大量緩存鍵同時過期:①、設置隨機時間:在設置緩存鍵的過期時間時,為每個鍵的過期時間添加一個隨機的偏移量,避免它們集中在同一時刻過期。②、設置key用不過期。
-
redis集群實現服務高可用:使用緩存服務器的集群模式,集群模式可以將數據分散存儲在多個節點上,當某個節點出現故障時,其他節點仍然可以正常提供服務,保證緩存服務的可用性。
-
多緩存結合:采用多級緩存架構,例如同時使用本地緩存(ehcache)+redis緩存。
-
服務降級:在應用層對請求進行限流,當請求量超過一定閾值時,直接拒絕部分請求,避免過多的請求直接訪問數據庫。
三、緩存穿透
(一)、名詞解釋
緩存穿透是指客戶端請求的數據在緩存中不存在,同時在數據庫中也不存在,這樣每次該請求都會穿透緩存,直接訪問數據庫。如果有大量這樣的無效請求持續涌入,會對數據庫造成極大的壓力,甚至可能導致數據庫不堪重負而崩潰。例如,黑客可能會故意發起大量不存在的鍵的請求,以消耗數據庫資源。
(二)、發生場景
- 黑客惡意攻擊:攻擊者可能會利用系統的漏洞,構造大量不存在的請求,如不存在的用戶 ID、商品 ID 等,向系統發起請求。由于這些請求對應的數據在緩存和數據庫中都不存在,會導致大量請求直接穿透緩存訪問數據庫,從而影響系統的正常運行。
- 業務數據異常:在業務系統中,可能會出現數據不一致或者數據刪除不及時的情況。
- 錯誤的用戶輸入:如果系統沒有對用戶輸入進行嚴格的驗證,用戶可能會輸入錯誤的查詢條件,如輸入一個不存在的訂單號、手機號碼等。這些無效請求會直接穿透緩存訪問數據庫。
(三)、預防與解決措施
1、方案一
緩存空值或默認值,當查詢的數據在數據庫中不存在時,在緩存中存儲一個空值或者默認值,并設置一個較短的過期時間。這樣下次相同的請求就可以直接從緩存中獲取空值或默認值,而不會再穿透到數據庫。
2、方案二
使用布隆過濾器,①、自研布隆過濾器。②、使用Google? Guava 庫實現布隆過濾器。
(四)、案例演示
自研簡略版布隆過濾器在上一篇已經學習過了,下面將學習Google Guava實現方式。Guava源碼地址:GitHub - google/guava: Google core libraries for Java
1、需求說明
模擬使用Guava布隆過濾器攔截掉非法數字,對于合法的數字放行。
2、導入依賴
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.2-jre</version>
</dependency>
3、編碼實現
public class GuavaBloomFilterDemo {public static void main(String[] args) {//誤判率,它越小誤判的個數也就越少(思考,是不是可以設置的無限小,沒有誤判豈不更好)//fpp the desired false positive probability 0.0 < fpp < 1.0,誤判率越低,消耗的資源越多,哈希函數也用的越多,誤判率也就越低// 默認值 0.03BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000000, 0.03);//1 先往布隆過濾器里面插入100萬的樣本數據for (int i = 1; i <= 1000000; i++) {bloomFilter.put(i);}List<Integer> list1 = Arrays.asList(120000000, 111111, 10, 222222222, 42575, 123457);list1.forEach(e -> {boolean result = bloomFilter.mightContain(e);if (result) {// TODO 查redis → 查數據庫 → .....System.out.println("存在,放行" + e);} else {// 直接返回結果System.out.println("不存在,攔截" + e);}});//故意取10萬個不在過濾器里的值,看看有多少個會被認為在過濾器里List<Integer> list = new ArrayList<>(1000000);for (int i = 1000000 + 1; i <= 1100000; i++) {if (bloomFilter.mightContain(i)) {list.add(i);}}System.out.println("誤判的總數量::{}" + list.size());}
}
四、緩存擊穿
(一)、名詞解釋
緩存擊穿是指在高并發的場景下,一個非常熱點的 key 在緩存中過期失效的瞬間,大量針對該 key 的請求同時涌入。由于此時緩存中沒有該 key 對應的數據,這些請求就會全部轉向數據庫去查詢。數據庫在短時間內需要處理大量的查詢請求,從而承受巨大的壓力,可能會導致數據庫性能急劇下降,甚至出現崩潰的情況,進而影響整個系統的正常運行。
(二)、發生場景
- 熱點數據過期:在電商系統中,一款熱門商品的信息會被大量用戶頻繁訪問,為了減輕數據庫壓力,會將該商品信息緩存起來并設置過期時間。當這個過期時間到達,緩存中的數據失效,而此時恰好有大量用戶同時發起對該商品信息的請求,就會出現緩存擊穿的情況。
- 流量突發:一些突發的熱點事件會導致瞬間產生大量的請求。比如,某明星突然發布一條微博,引發大量粉絲同時訪問其個人主頁,而該主頁信息在緩存中過期,大量請求就會直接沖向數據庫。
- 數據預熱不正確:比如新聞網站在每天早上進行緩存預熱,但遺漏了當天的一條重大熱點新聞,當用戶大量訪問該新聞時,就會出現問題。
(三)、預防與解決措施
1、方案一
使用雙檢加鎖策略。前面已經學習過了,不在闡述。
public String get(String key) {String value = redis.get(key);// 查詢緩存if (value != null) {//緩存存在直接返回return value;} else {//緩存不存在則對方法加鎖//假設請求量很大,緩存過期synchronized (this) {value = redis.get(key); // 在查一遍redisif (value == null) {// 從數據庫獲取數據value = dao.get(key);// 設置過期時間并回寫到緩存redis.setex(key, time, value);}return value;}}}
2、方案二
隨機退避策略。當發現緩存中熱點 key 失效時,讓各個請求不要立即去訪問數據庫,而是各自隨機等待一段不同的時間后再去嘗試獲取數據。這樣可以避免大量請求在同一時刻集中訪問數據庫,將請求的時間分散開,減輕數據庫在短時間內的壓力。
public String randomBackoffMethod(String key) {try {Jedis jedis = RedisUtils.getJedis();String data = jedis.get(key);if (data == null) {try {// 生成隨機退避時間 毫秒int backoffTime = new Random().nextInt(2000);Thread.sleep(backoffTime);// 再次嘗試從緩存獲取數據,// 但是在高并發場景下,可能線程隨機退避時間會一樣,// 為了避免造成緩存雙寫不一致問題,使用雙檢鎖策略來防止String value = jedis.get(key);// 查詢緩存if (value != null) {//緩存存在直接返回return value;} else {//緩存不存在則對方法加鎖//假設請求量很大,緩存過期synchronized (this) {value = jedis.get(key); // 在查一遍redisif (value == null) {// 從數據庫獲取數據value = dao.get(key);// 設置過期時間并回寫到緩存jedis.setex(key, time, value);}return value;}}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}jedis.close();return data;} catch (Exception e) {e.printStackTrace();}return null;}
3、方案三
差異失效策略。它的核心思想是避免多個熱點 Key 在同一時刻同時失效,從而防止大量請求在瞬間全部涌向數據庫,給數據庫造成過大壓力。
在緩存擊穿場景中的實現方式為一個熱點key拷貝兩份,兩份緩存過期時間不一樣,將緩存失效的時間分散開來,以此保障系統的穩定性與性能。
/*** 差異失效策略查詢*/public String differentialFailureSelectMethod(String key) {// 同一個熱點key的前綴String prefixA = "keyA:";String prefixB = "keyB:";try {Jedis jedis = RedisUtils.getJedis();String data = jedis.get(prefixA + key);if (data == null) {System.out.println("=========A緩存已經失效");//用戶先查詢緩存A(上面的代碼),如果緩存A查詢不到(例如,更新緩存的時候刪除了),再查詢緩存Bdata = jedis.get(prefixB + key);if (data == null) {System.out.println("=========B緩存已經失效");//TODO 查數據庫 → 回寫redis}return data;}System.out.println("查詢結果:{}" + data);} catch (Exception ex) {ex.printStackTrace();}return null;}/*** 差異失效策略更新*/public void differentialFailureUpdateMethod(String key) {try {// 以前的某個熱點keyString oldHotKeyA = "oldHotKey:" + key;String oldHotKeyB = "oldHotKey:" + key;// 新熱點key前綴String prefixA = "newKeyA:";String prefixB = "newKeyB:";//模擬從數據庫查數據Jedis jedis = RedisUtils.getJedis();Object o = dao.get(key);//先更新B緩存jedis.del(oldHotKeyB);jedis.set(prefixB + o.getId());jedis.expire(prefixB + o.getId(), 2000);//再更新A緩存jedis.del(oldHotKeyA);jedis.set(prefixA + o.getId());jedis.expire(prefixA + o.getId(), 1000);} catch (Exception e) {e.printStackTrace();}}
五、總結?
一圖總結,四個問題以及解決辦法:
問題類型 | 核心問題 | 解決方案 | 典型場景 |
---|---|---|---|
緩存預熱 | 數據未提前加載 | 啟動加載、定時任務、腳本 | 電商大促前的商品信息加載 |
緩存雪崩 | 大量緩存失效 | 隨機過期、集群、高可用、限流 | 大量key過期、緩存服務器宕機時 |
緩存穿透 | 無效請求攻擊 | 布隆過濾器、緩存空值 | 惡意攻擊場景 |
緩存擊穿 | 熱點 Key 失效 | 雙檢鎖、差異失效、隨機退避 | 秒殺活動中的商品信息訪問 |
ps:努力到底,讓持續學習成為貫穿一生的堅守。學習筆記持續更新中。。。。