硬核!我花5小時肝出這篇Redis緩存解決方案,帶你起飛!

寫在前面

對於緩存穿透,雪崩相信不少小夥伴都有聽過,無論是工做中仍是面試都熱點問題,本文重點帶你們分析這些問題,給位看官請往下看!java

同時用XMind畫了一張導圖記錄Redis的學習筆記和一些面試解析(源文件對部分節點有詳細備註和參考資料,歡迎關注個人公衆號:阿風的架構筆記 後臺發送【導圖】拿下載連接, 已經完善更新): 面試

1、緩存穿透

1. 什麼是緩存穿透?

爲了緩解持久層數據庫的壓力,在服務器和存儲層之間添加了一層緩存;redis

一個簡單的正常請求: 當客戶端發起請求時,服務器響應處理,會先從redis緩存層查詢客戶端須要的請求數據,若是緩存層有緩存的數據,會將數據返回給服務器,服務器再返回給客戶端;若是緩存層中沒有客戶端須要的數據,則會去底層存儲層查找,再返回給服務器;算法

緩存穿透就是: 當客戶端想要查詢一個數據,發現redis緩存層中沒有(即緩存沒有命中),因而向持久層數據庫查詢,發現也沒有,因而本次查詢失敗;當用戶不少的時候,緩存都沒有命中,因而都去請求了持久層數據庫,此時會給持久層數據庫形成很大的壓力,這時候就至關於出現了緩存穿透。數據庫

2. 解決辦法

在緩存層加布隆過濾器,通俗簡述一下其做用:將數據庫中的 id ,經過某方式映射到布隆過濾器,當處理不存在的 id 時,布隆過濾器會將該請求過直接過濾出去,不會到數據庫作操做。數組

3. 布隆過濾器

1)概述: 布隆過濾器是一種數據結構,比較巧妙的機率型數據結構,其實是一個很長的二進制向量和一系列隨機映射函數,特色是高效地插入和查詢,能夠用來告訴你 「某樣東西必定不存在或者可能存在」,相比於傳統的 List、Set、Map 等數據結構,它更高效、佔用空間更少,可是缺點是其返回的結果是機率性的,而不是確切的。緩存

2)返回結果的不確切性: 布隆過濾器是一個 bit 向量或者說 bit 數組:假設有8位服務器

映射數據1: 使用多個不一樣的哈希函數生成多個哈希值,並對每一個生成的哈希值指向的 bit 位置爲 1,好比三次hash完後,data1 將一、三、6位,置爲1;markdown

映射數據2: data2 將二、三、6位,置爲1,此時因爲hash爲隨機性,因此6位和 data1 有重複的,便會覆蓋 data1 的第6位的1;網絡

問題來了!!

6 這個 bit 位因爲兩個值的哈希函數都返回了這個 bit 位,所以它被覆蓋了,當咱們若是想查詢 data3這個值是否存在,假設哈希函數返回了 一、五、6三個值,結果咱們發現 5 這個 bit 位上的值爲 0,說明沒有任何一個值映射到這個 bit 位上,所以咱們能夠很肯定地說 data3 這個值不存在。而當咱們須要查詢 data1 這個值是否存在的話,那麼哈希函數必然會返回 一、三、6,而後咱們檢查發現這三個 bit 位上的值均爲 1,那麼咱們是否能夠說 data1 存在了麼?答案是不能夠,只能是 data1 這個值可能存在!由於隨着增長的值愈來愈多,被置爲 1 的 bit 位也會愈來愈多,這樣某個值 data4 即便沒有被存儲過,可是萬一哈希函數返回的三個 bit 位都被其餘位置位了 1 ,那麼程序仍是會判斷 data4 這個值存在。

因此: 布隆過濾器的長度會直接影響誤報率,布隆過濾器越長且誤報率越小。

3)簡單剖析布隆過濾器源碼

導入guava的包:

<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>23.0</version>
</dependency>    
複製代碼

源碼: BloomFilter一共四個create方法,最終都是走向第四個方法;

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
        return create(funnel, (long) expectedInsertions);
    }  

    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
        return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
    }

    public static <T> BloomFilter<T> create( Funnel<? super T> funnel, long expectedInsertions, double fpp) {
        return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
    }

    static <T> BloomFilter<T> create( Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
     ......
    }
複製代碼

參數類型: funnel:數據類型;expectedInsertions:指望插入的值的個數;fpp:錯誤率(默認值爲0.03);strategy:哈希算法。

總結: 錯誤率越大,所需空間和時間越小;反之錯誤率越小,所需空間和時間越大!

2、緩存擊穿

一、什麼是緩存擊穿?

在日常高併發的系統中,大量的請求同時查詢一個key時,此時這個key正好失效了,就會致使大量的請求都打到數據庫上面去。這種現象咱們稱爲緩存擊穿

二、問題排查

  1. Redis中某個key過時,該key訪問量巨大
  2. 多個數據請求從服務器直接壓到Redis後,均未命中
  3. Redis在短期內發起了大量對數據庫中同一數據的訪問

三、如何解決

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. "永遠不過時"

  • 從redis上看,確實沒有設置過時時間,這就保證了,不會出現熱點key過時問題,也就是「物理」不過時。

  • 從功能上看,若是不過時,那不就成靜態的了嗎?因此咱們把過時時間存在key對應的value裏,若是發現要過時了,經過一個後臺的異步線程進行緩存的構建,也就是「邏輯」過時

    img

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);
        }
    }
}
複製代碼

4.總結

緩存擊穿就是單個高熱數據過時的瞬間,數據訪問量較大,未命中redis後,發起了大量對同一數據的數據庫訪問,致使對數據庫服務器形成壓力。應對策略應該在業務數據分析與預防方面進行,配合運行監控測試與即時調整策略,畢竟單個key的過時監控難度較高,配合雪崩處理策略便可。

3、緩存雪崩

1. 什麼是緩存雪崩?

緩存雪崩是指: 某一時間段,緩存集中過時失效,即緩存層出現了錯誤,不能正常工做了;因而全部的請求都會達到存儲層,存儲層的調用量會暴增,形成 「雪崩」;

**好比:**雙十二臨近12點,搶購商品,此時會設置商品在緩存區,設置過時時間爲1小時,當到了1點時,緩存過時,全部的請求會落到存儲層,此時數據庫可能扛不住壓力,天然 「掛掉」。

2. 解決辦法

redis高可用

這個思想的含義是,既然redis有可能掛掉,那我多增設幾臺redis,這樣一臺掛掉以後其餘的還能夠繼續工做,其實就是搭建的集羣

限流降級

這個解決方案的思想是,在緩存失效後,經過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。好比對某個key只容許一個線程查詢數據和寫緩存,其餘線程等待。

數據預熱

數據預熱的含義就是在正式部署以前,我先把可能的數據先預先訪問一遍,這樣部分可能大量訪問的數據就會加載到緩存中,在即將發生大併發訪問前手動觸發加載緩存不一樣的key,設置不一樣的過時時間,讓緩存失效的時間點儘可能均勻。

4、緩存預熱

1.什麼是緩存預熱

緩存預熱就是系統上線後,將相關的緩存數據直接加載到緩存系統。這樣就能夠避免在用戶請求的時候,先查詢數據庫,而後再將數據緩存的問題。用戶直接查詢事先被預熱的緩存數據。如圖所示:

img

若是不進行預熱, 那麼 Redis 初識狀態數據爲空,系統上線初期,對於高併發的流量,都會訪問到數據庫中, 對數據庫形成流量的壓力。

2.問題排查

  1. 請求數量較高
  2. 主從之間數據吞吐量較大,數據同步操做頻度較高

3.有什麼解決方案?

前置準備工做:

  1. 平常例行統計數據訪問記錄,統計訪問頻度較高的熱點數據

  2. 利用LRU數據刪除策略,構建數據留存隊列

    例如:storm與kafka配合
    複製代碼

準備工做:

  1. 將統計結果中的數據分類,根據級別,redis優先加載級別較高的熱點數據
  2. 利用分佈式多服務器同時進行數據讀取,提速數據加載過程
  3. 熱點數據主從同時預熱

實施:

  1. 使用腳本程序固定觸發數據預熱過程
  2. 若是條件容許,使用了CDN(內容分發網絡),效果會更好

4.總結

緩存預熱就是系統啓動前,提早將相關的緩存數據直接加載到緩存系統。避免在用戶請求的時候,先查詢數據庫,而後再將數據緩存的問題!用戶直接查詢事先被預熱的緩存數據

5、緩存降級

降級的狀況,就是緩存失效或者緩存服務掛掉的狀況下,咱們也不去訪問數據庫。咱們直接訪問內存部分數據緩存或者直接返回默認數據。

舉例來講:

對於應用的首頁,通常是訪問量很是大的地方,首頁裏面每每包含了部分推薦商品的展現信息。這些推薦商品都會放到緩存中進行存儲,同時咱們爲了不緩存的異常狀況,對熱點商品數據也存儲到了內存中。同時內存中還保留了一些默認的商品信息。以下圖所示:

img

降級通常是有損的操做,因此儘可能減小降級對於業務的影響程度。

看完三件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。
  2. 關注公衆號 『 阿風的架構筆記 』,不按期分享原創知識。
  3. 同時能夠期待後續文章ing🚀
相關文章
相關標籤/搜索