你們好,我是清風!以前分享過大廠Redis高併發場景設計,面試問的都在這!及互聯網大廠Java工程師面試指南——Redis篇,今天給小夥伴說說大廠面試高頻必問點(緩存穿透,雪崩等問題)!以爲不錯的小夥伴能夠關注點贊一下,感謝支持!java
2020年面試必備的Java後端進階面試題總結了一份複習指南在Github上,內容詳細,圖文並茂,有須要學習的朋友能夠Star一下! GitHub地址: github.com/Java-Ling/J…git
緩存雪崩是指在咱們設置緩存時採用了相同的過時時間,致使緩存在某一時刻同時失效,請求所有轉發到DB,DB瞬時壓力太重雪崩。因爲原有緩存失效,新緩存未到期間全部本來應該訪問緩存的請求都去查詢數據庫了,而對數據庫CPU和內存形成巨大壓力,嚴重的會形成數據庫宕機。github
緩存雪崩就是瞬間過時數據量太大,致使對數據庫服務器形成壓力。如可以有效避免過時時間集中,能夠有效解決雪崩現象的出現(約40%),配合其餘策略一塊兒使用,並監控服務器的運行數據,根據運行記錄作快速調整。面試
緩存預熱就是系統上線後,將相關的緩存數據直接加載到緩存系統。這樣就能夠避免在用戶請求的時候,先查詢數據庫,而後再將數據緩存的問題。用戶直接查詢事先被預熱的緩存數據。如圖所示:redis
若是不進行預熱, 那麼 Redis 初識狀態數據爲空,系統上線初期,對於高併發的流量,都會訪問到數據庫中, 對數據庫形成流量的壓力。sql
前置準備工做:數據庫
平常例行統計數據訪問記錄,統計訪問頻度較高的熱點數據後端
利用LRU數據刪除策略,構建數據留存隊列緩存
例如:storm與kafka配合
複製代碼
準備工做: 3. 將統計結果中的數據分類,根據級別,redis優先加載級別較高的熱點數據 4. 利用分佈式多服務器同時進行數據讀取,提速數據加載過程 5. 熱點數據主從同時預熱服務器
實施: 6. 使用腳本程序固定觸發數據預熱過程 7. 若是條件容許,使用了CDN(內容分發網絡),效果會更好
緩存預熱就是系統啓動前,提早將相關的緩存數據直接加載到緩存系統。避免在用戶請求的時候,先查詢數據庫,而後再將數據緩存的問題!用戶直接查詢事先被預熱的緩存數據
緩存穿透是指用戶查詢數據,在數據庫沒有,天然在緩存中也不會有。這樣就致使用戶查詢的時候,在緩存中找不到對應key的value,每次都要去數據庫再查詢一遍,而後返回空(至關於進行了兩次無用的查詢)。這樣請求就繞過緩存直接查數據庫
一、緩存空值
若是一個查詢返回的數據爲空(無論是數據不存在,仍是系統故障)咱們仍然把這個空結果進行緩存,但它的過時時間會很短,最長不超過5分鐘。經過這個設置的默認值存放到緩存,這樣第二次到緩存中獲取就有值了,而不會繼續訪問數據庫
二、採用布隆過濾器BloomFilter
**優點:**佔用內存空間很小,位存儲;性能特別高,使用key的hash判斷key存不存在
將全部可能存在的數據哈希到一個足夠大的bitmap中,一個必定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力
在緩存以前在加一層BloomFilter,在查詢的時候先去BloomFilter去查詢key是否存在,若是不存在就直接返回,存在再去查詢緩存,緩存中沒有再去查詢數據庫
緩存擊穿訪問了不存在的數據,跳過了合法數據的redis數據緩存階段,每次訪問數據庫,致使對數據庫服務器形成壓力。一般此類數據的出現量是一個較低的值,當出現此類狀況以毒攻毒,並及時報警。應對策略應該在臨時預案防範方面多作文章。不管是黑名單仍是白名單,都是對總體系統的壓力,警報解除後儘快移除。
降級的狀況,就是緩存失效或者緩存服務掛掉的狀況下,咱們也不去訪問數據庫。咱們直接訪問內存部分數據緩存或者直接返回默認數據。
舉例來講:
對於應用的首頁,通常是訪問量很是大的地方,首頁裏面每每包含了部分推薦商品的展現信息。這些推薦商品都會放到緩存中進行存儲,同時咱們爲了不緩存的異常狀況,對熱點商品數據也存儲到了內存中。同時內存中還保留了一些默認的商品信息。以下圖所示:
降級通常是有損的操做,因此儘可能減小降級對於業務的影響程度。
在日常高併發的系統中,大量的請求同時查詢一個key時,此時這個key正好失效了,就會致使大量的請求都打到數據庫上面去。這種現象咱們稱爲緩存擊穿
1. 使用互斥鎖(mutex key)
這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其餘線程等待構建緩存的線程執行完,從新從緩存獲取數據就能夠了。若是是單機,能夠用synchronized或者lock來處理,若是是分佈式環境能夠用分佈式鎖就能夠了(分佈式鎖,能夠用memcache的add, redis的setnx, zookeeper的添加節點操做)。
2. "提早"使用互斥鎖(mutex key)
在value內部設置1個超時值(timeout1), timeout1比實際的redis timeout(timeout2)小。當從cache讀取到timeout1發現它已通過期時候,立刻延長timeout1並從新設置到cache。而後再從數據庫加載數據並設置到cache中
3. "永遠不過時"
4. 緩存屏障
class MyCache{ private ConcurrentHashMap<String, String> map; private CountDownLatch countDownLatch; private AtomicInteger atomicInteger; public MyCache(ConcurrentHashMap<String, String> map, CountDownLatch countDownLatch, AtomicInteger atomicInteger) { this.map = map; this.countDownLatch = countDownLatch; this.atomicInteger = atomicInteger; } public String get(String key){ String value = map.get(key); if (value != null){ System.out.println(Thread.currentThread().getName()+"\t 線程獲取value值 value="+value); return value; } // 若是沒獲取到值 // 首先嚐試獲取token,而後去查詢db,初始化化緩存; // 若是沒有獲取到token,超時等待 if (atomicInteger.compareAndSet(0,1)){ System.out.println(Thread.currentThread().getName()+"\t 線程獲取token"); return null; } // 其餘線程超時等待 try { System.out.println(Thread.currentThread().getName()+"\t 線程沒有獲取token,等待中。。。"); countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } // 初始化緩存成功,等待線程被喚醒 // 等待線程等待超時,自動喚醒 System.out.println(Thread.currentThread().getName()+"\t 線程被喚醒,獲取value ="+map.get("key")); return map.get(key); } public void put(String key, String value){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); // 更新狀態 atomicInteger.compareAndSet(1, 2); // 通知其餘線程 countDownLatch.countDown(); System.out.println(); System.out.println(Thread.currentThread().getName()+"\t 線程初始化緩存成功!value ="+map.get("key")); } } class MyThread implements Runnable{ private MyCache myCache; public MyThread(MyCache myCache) { this.myCache = myCache; } @Override public void run() { String value = myCache.get("key"); if (value == null){ myCache.put("key","value"); } } } public class CountDownLatchDemo { public static void main(String[] args) { MyCache myCache = new MyCache(new ConcurrentHashMap<>(), new CountDownLatch(1), new AtomicInteger(0)); MyThread myThread = new MyThread(myCache); ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { executorService.execute(myThread); } } } 複製代碼
緩存擊穿就是單個高熱數據過時的瞬間,數據訪問量較大,未命中redis後,發起了大量對同一數據的數據庫訪問,致使對數據庫服務器形成壓力。應對策略應該在業務數據分析與預防方面進行,配合運行監控測試與即時調整策略,畢竟單個key的過時監控難度較高,配合雪崩處理策略便可。
這些都是實際項目中,可能碰到的一些問題,也是面試的時候常常會被問到的知識點,實際上還有不少不少各類各樣的問題,文中的解決方案,也不可能知足全部的場景,相對來講只是對該問題的入門解決方法。通常正式的業務場景每每要複雜的多,應用場景不一樣,方法和解決方案也不一樣,因爲上述方案,考慮的問題並非很全面,所以並不適用於正式的項目開發,可是能夠做爲概念理解入門,具體解決方案要根據實際狀況來肯定!