看完這篇Redis緩存三大問題兑障,保你能和面試官互扯侄非。

來自公眾號:非科班的科班
作者:非科班的科班

日常的開發(fā)中,無不都是使用數(shù)據(jù)庫來進行數(shù)據(jù)的存儲流译,由于一般的系統(tǒng)任務(wù)中通常不會存在高并發(fā)的情況逞怨,所以這樣看起來并沒有什么問題。

一旦涉及大數(shù)據(jù)量的需求福澡,如一些商品搶購的情景叠赦,或者主頁訪問量瞬間較大的時候,單一使用數(shù)據(jù)庫來保存數(shù)據(jù)的系統(tǒng)會因為面向磁盤革砸,磁盤讀/寫速度問題有嚴重的性能弊端除秀,詳細的磁盤讀寫原理請參考這一片[]糯累。

在這一瞬間成千上萬的請求到來,需要系統(tǒng)在極短的時間內(nèi)完成成千上萬次的讀/寫操作册踩,這個時候往往不是數(shù)據(jù)庫能夠承受的泳姐,極其容易造成數(shù)據(jù)庫系統(tǒng)癱瘓,最終導(dǎo)致服務(wù)宕機的嚴重生產(chǎn)問題暂吉。

為了克服上述的問題胖秒,項目通常會引入NoSQL技術(shù),這是一種基于內(nèi)存數(shù)據(jù)庫慕的,并且提供一定的持久化功能阎肝。

Redis技術(shù)就是NoSQL技術(shù)中的一種。Redis緩存的使用业稼,極大的提升了應(yīng)用程序的性能和效率盗痒,特別是數(shù)據(jù)查詢方面。

但同時低散,它也帶來了一些問題俯邓。其中,最要害的問題熔号,就是數(shù)據(jù)的一致性問題稽鞭,從嚴格意義上講,這個問題無解引镊。如果對數(shù)據(jù)的一致性要求很高朦蕴,那么就不能使用緩存

另外的一些典型問題就是弟头,緩存穿透吩抓、緩存擊穿緩存雪崩。本篇文章從實際代碼操作赴恨,來提出解決這三個緩存問題的方案疹娶,畢竟Redis的緩存問題是實際面試中高頻問點,理論和實操要兼得伦连。

緩存穿透

緩存穿透是指查詢一條數(shù)據(jù)庫和緩存都沒有的一條數(shù)據(jù)雨饺,就會一直查詢數(shù)據(jù)庫,對數(shù)據(jù)庫的訪問壓力就會增大惑淳,緩存穿透的解決方案额港,有以下兩種:

  1. 緩存空對象:代碼維護較簡單,但是效果不好歧焦。

  2. 布隆過濾器:代碼維護復(fù)雜移斩,效果很好。

緩存空對象

緩存空對象是指當一個請求過來緩存中和數(shù)據(jù)庫中都不存在該請求的數(shù)據(jù),第一次請求就會跳過緩存進行數(shù)據(jù)庫的訪問叹哭,并且訪問數(shù)據(jù)庫后返回為空忍宋,此時也將該空對象進行緩存。

若是再次進行訪問該空對象的時候风罩,就會直接擊中緩存,而不是再次數(shù)據(jù)庫舵稠,緩存空對象實現(xiàn)的原理圖如下:

image

緩存空對象的實現(xiàn)代碼如下:

public class UserServiceImpl {
     @Autowired
     UserDAO userDAO;
     @Autowired
     RedisCache redisCache;

     public User findUser(Integer id) {
          Object object = redisCache.get(Integer.toString(id));
          // 緩存中存在超升,直接返回
          if(object != null) {
               // 檢驗該對象是否為緩存空對象,是則直接返回null
               if(object instanceof NullValueResultDO) {
                    return null;
               }
               return (User)object;
          } else {  
               // 緩存中不存在哺徊,查詢數(shù)據(jù)庫
               User user = userDAO.getUser(id);
               // 存入緩存
               if(user != null) {
                    redisCache.put(Integer.toString(id),user);
               } else {
                    // 將空對象存進緩存
                    redisCache.put(Integer.toString(id), new NullValueResultDO());
               }
               return user;
          }
     }          
}

緩存空對象的實現(xiàn)代碼很簡單室琢,但是緩存空對象會帶來比較大的問題,就是緩存中會存在很多空對象落追,占用內(nèi)存的空間盈滴,浪費資源,一個解決的辦法就是設(shè)置空對象的較短的過期時間轿钠,代碼如下:

// 在緩存的時候巢钓,添加多一個該空對象的過期時間60秒
redisCache.put(Integer.toString(id), new NullValueResultDO(),60);
布隆過濾器

布隆過濾器是一種基于概率數(shù)據(jù)結(jié)構(gòu),主要用來判斷某個元素是否在集合內(nèi)疗垛,它具有運行速度快(時間效率)症汹,占用內(nèi)存小的優(yōu)點(空間效率),但是有一定的誤識別率刪除困難的問題贷腕。它只能告訴你某個元素一定不在集合內(nèi)或可能在集合內(nèi)背镇。

在計算機科學(xué)中有一種思想:空間換時間,時間換空間泽裳。一般兩者是不可兼得瞒斩,而布隆過濾器運行效率和空間大小都兼得,它是怎么做到的呢涮总?

在布隆過濾器中引用了一個誤判率的概念胸囱,即它可能會把不屬于這個集合的元素認為可能屬于這個集合,但是不會把屬于這個集合的認為不屬于這個集合妹卿,布隆過濾器的特點如下:

  1. 一個非常大的二進制位數(shù)組 (數(shù)組里只有0和1)

  2. 若干個哈希函數(shù)

  3. 空間效率查詢效率高

  4. 不存在漏報(False Negative):某個元素在某個集合中旺矾,肯定能報出來。

  5. 可能存在誤報(False Positive):某個元素不在某個集合中夺克,可能也被爆出來箕宙。

  6. 不提供刪除方法,代碼維護困難铺纽。

  7. 位數(shù)組初始化都為0柬帕,它不存元素的具體值,當元素經(jīng)過哈希函數(shù)哈希后的值(也就是數(shù)組下標)對應(yīng)的數(shù)組位置值改為1。

實際布隆過濾器存儲數(shù)據(jù)和查詢數(shù)據(jù)的原理圖如下:

image

可能很多讀者看完上面的特點和原理圖陷寝,還是看不懂锅很,別急下面通過圖解一步一步的講解布隆過濾器,總而言之一句簡單的話概括就是布隆過濾器是一個很大二進制位數(shù)組凤跑,數(shù)組里面只存0和1爆安。

初始化的布隆過濾器的結(jié)構(gòu)圖如下:

image

以上只是畫了布隆過濾器的很小很小的一部分,實際布隆過濾器是非常大的數(shù)組(這里的大是指它的長度大仔引,并不是指它所占的內(nèi)存空間大)扔仓。

那么一個數(shù)據(jù)是怎么存進布隆過濾器的呢?

當一個數(shù)據(jù)進行存入布隆過濾器的時候咖耘,會經(jīng)過如干個哈希函數(shù)進行哈希(若是對哈希函數(shù)還不懂的請參考這一片[])翘簇,得到對應(yīng)的哈希值作為數(shù)組的下標,然后將初始化的位數(shù)組對應(yīng)的下標的值修改為1儿倒,結(jié)果圖如下:

image

當再次進行存入第二個值的時候版保,修改后的結(jié)果的原理圖如下:


image

所以每次存入一個數(shù)據(jù),就會哈希函數(shù)的計算夫否,計算的結(jié)果就會作為下標彻犁,在布隆過濾器中有多少個哈希函數(shù)就會計算出多少個下標,布隆過濾器插入的流程如下:

  1. 將要添加的元素給m個哈希函數(shù)

  2. 得到對應(yīng)于位數(shù)組上的m個位置

  3. 將這m個位置設(shè)為1

那么為什么會有誤判率呢慷吊?

假設(shè)在我們多次存入值后袖裕,在布隆過濾器中存在x、y溉瓶、z這三個值急鳄,布隆過濾器的存儲結(jié)構(gòu)圖如下所示:

image

當我們要查詢的時候,比如查詢a這個數(shù)堰酿,實際中a這個數(shù)是不存在布隆過濾器中的疾宏,經(jīng)過2個哈希函數(shù)計算后得到a的哈希值分別為2和13,結(jié)構(gòu)原理圖如下:


image

經(jīng)過查詢后触创,發(fā)現(xiàn)2和13位置所存儲的值都為1坎藐,但是2和13的下標分別是x和z經(jīng)過計算后的下標位置的修改,該布隆過濾器中實際不存在a哼绑,那么布隆過濾器就會誤判改值可能存在岩馍,因為布隆過濾器不存元素值,所以存在誤判率抖韩。

那么具體布隆過布隆過濾的判斷的準確率和一下兩個因素有關(guān):

  1. 布隆過濾器大小:越大蛀恩,誤判率就越小,所以說布隆過濾器一般長度都是非常大的茂浮。

  2. 哈希函數(shù)的個數(shù):哈希函數(shù)的個數(shù)越多双谆,那么誤判率就越小壳咕。

那么為什么不能刪除元素呢?

原因很簡單顽馋,因為刪除元素后谓厘,將對應(yīng)元素的下標設(shè)置為零,可能別的元素的下標也引用改下標寸谜,這樣別的元素的判斷就會收到影響竟稳,原理圖如下:

image

當你刪除z元素之后,將對應(yīng)的下標10和13設(shè)置為0程帕,這樣導(dǎo)致x和y元素的下標受到影響住练,導(dǎo)致數(shù)據(jù)的判斷不準確,所以直接不提供刪除元素的api愁拭。

以上說的都是布隆過濾器的原理,只有理解了原理亏吝,在實際的運用才能如魚得水岭埠,下面就來實操代碼,手寫一個簡單的布隆過濾器蔚鸥。

對于要手寫一個布隆過濾器惜论,首先要明確布隆過濾器的核心:

  • 若干哈希函數(shù)

  • 存值的Api

  • 判斷值得Api

實現(xiàn)的代碼如下:

public class MyBloomFilter {
    // 布隆過濾器長度
    private static final int SIZE = 2 << 10;
    // 模擬實現(xiàn)不同的哈希函數(shù)
    private static final int[] num= new int[] {5, 19, 23, 31,47, 71};   
    // 初始化位數(shù)組
    private BitSet bits = new BitSet(SIZE);
    // 用于存儲哈希函數(shù)
    private MyHash[] function = new MyHash[num.length];

    // 初始化哈希函數(shù)
    public MyBloomFilter() {
        for (int i = 0; i < num.length; i++) {
            function [i] = new MyHash(SIZE, num[i]);
        }
    }

    // 存值A(chǔ)pi 
    public void add(String value) {
        // 對存入得值進行哈希計算
        for (MyHash f: function) {
            // 將為數(shù)組對應(yīng)的哈希下標得位置得值改為1
            bits.set(f.hash(value), true);
        }
    }

    // 判斷是否存在該值得Api
    public boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean result= true;
        for (MyHash f : func) {
            result= result&& bits.get(f.hash(value));
        }
        return result;
    }
}

哈希函數(shù)代碼如下:

public static class MyHash {
        private int cap;
        private int seed;
        // 初始化數(shù)據(jù)
        public MyHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }
        // 哈希函數(shù)
        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }

布隆過濾器測試代碼如下:

    public static void test {
        String value = "4243212355312";
        MyBloomFilter filter = new MyBloomFilter();
        System.out.println(filter.contains(value));
        filter.add(value);
        System.out.println(filter.contains(value));
    }

以上就是手寫了一個非常簡單的布隆過濾器,但是實際項目中可能由牛人或者大公司已經(jīng)幫你寫好的止喷,如谷歌的Google Guava馆类,只需要在項目中引入一下依賴:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0.1-jre</version>
</dependency>

實際項目中具體的操作代碼如下:

public static void MyBloomFilterSysConfig {

     @Autowired
     OrderMapper orderMapper

    // 1.創(chuàng)建布隆過濾器  第二個參數(shù)為預(yù)期數(shù)據(jù)量10000000,第三個參數(shù)為錯誤率0.00001
    BloomFilter<CharSequence> bloomFilter =  BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000, 0.00001);
    // 2.獲取所有的訂單弹谁,并將訂單的id放進布隆過濾器里面
    List<Order> orderList = orderMapper.findAll()
    for (Order order;orderList ) {
        Long id = order.getId();
        bloomFilter.put("" + id);
    }
}

在實際項目中會啟動一個系統(tǒng)任務(wù)或者定時任務(wù)乾巧,來初始化布隆過濾器,將熱點查詢數(shù)據(jù)的id放進布隆過濾器里面预愤,當用戶再次請求的時候沟于,使用布隆過濾器進行判斷,改訂單的id是否在布隆過濾器中存在植康,不存在直接返回null旷太,具體操作代碼:

// 判斷訂單id是否在布隆過濾器中存在
bloomFilter.mightContain("" + id)

布隆過濾器的缺點就是要維持容器中的數(shù)據(jù),因為訂單數(shù)據(jù)肯定是頻繁變化的销睁,實時的要更新布隆過濾器中的數(shù)據(jù)為最新供璧。

緩存擊穿

緩存擊穿是指一個key非常熱點,在不停的扛著大并發(fā)冻记,大并發(fā)集中對這一個點進行訪問睡毒,當這個key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存檩赢,直接請求數(shù)據(jù)庫吕嘀,瞬間對數(shù)據(jù)庫的訪問壓力增大违寞。

緩存擊穿這里強調(diào)的是并發(fā),造成緩存擊穿的原因有以下兩個:

  1. 該數(shù)據(jù)沒有人查詢過 偶房,第一次就大并發(fā)的訪問趁曼。(冷門數(shù)據(jù))

  2. 添加到了緩存,reids有設(shè)置數(shù)據(jù)失效的時間 棕洋,這條數(shù)據(jù)剛好失效挡闰,大并發(fā)訪問(熱點數(shù)據(jù))

對于緩存擊穿的解決方案就是加鎖,具體實現(xiàn)的原理圖如下:

image

當用戶出現(xiàn)大并發(fā)訪問的時候掰盘,在查詢緩存的時候和查詢數(shù)據(jù)庫的過程加鎖摄悯,只能第一個進來的請求進行執(zhí)行,當?shù)谝粋€請求把該數(shù)據(jù)放進緩存中愧捕,接下來的訪問就會直接集中緩存奢驯,防止了緩存擊穿

業(yè)界比價普遍的一種做法次绘,即根據(jù)key獲取value值為空時瘪阁,鎖上,從數(shù)據(jù)庫中load數(shù)據(jù)后再釋放鎖邮偎。若其它線程獲取鎖失敗管跺,則等待一段時間后重試。這里要注意禾进,分布式環(huán)境中要使用分布式鎖豁跑,單機的話用普通的鎖(synchronizedLock)就夠了泻云。

下面以一個獲取商品庫存的案例進行代碼的演示艇拍,單機版的鎖實現(xiàn)具體實現(xiàn)的代碼如下:

// 獲取庫存數(shù)量
public String getProduceNum(String key) {
    try {
        synchronized (this) {   //加鎖
            // 緩存中取數(shù)據(jù),并存入緩存中
            int num= Integer.parseInt(redisTemplate.opsForValue().get(key));

            if (num> 0) {
                //沒查一次庫存-1
                redisTemplate.opsForValue().set(key, (num- 1) + "");
                System.out.println("剩余的庫存為num:" + (num- 1));
            } else {
                System.out.println("庫存為0");
            }
        }
    } catch (NumberFormatException e) {
        e.printStackTrace();
    } finally {
    }
    return "OK";
}

分布式的鎖實現(xiàn)具體實現(xiàn)的代碼如下:

public String getProduceNum(String key) {
    // 獲取分布式鎖
    RLock lock = redissonClient.getLock(key);
    try {
        // 獲取庫存數(shù)
        int num= Integer.parseInt(redisTemplate.opsForValue().get(key));  
        // 上鎖           
        lock.lock();
        if (num> 0) {
            //減少庫存壶愤,并存入緩存中
            redisTemplate.opsForValue().set(key, (num - 1) + "");
            System.out.println("剩余庫存為num:" + (num- 1));
        } else {
            System.out.println("庫存已經(jīng)為0");
        }
    } catch (NumberFormatException e) {
        e.printStackTrace();
    } finally {
        //解鎖
        lock.unlock();
    }
    return "OK";
}

緩存雪崩

緩存雪崩 是指在某一個時間段淑倾,緩存集中過期失效。此刻無數(shù)的請求直接繞開緩存征椒,直接請求數(shù)據(jù)庫娇哆。

造成緩存雪崩的原因,有以下兩種:

  1. reids宕機

  2. 大部分數(shù)據(jù)失效

比如天貓雙11勃救,馬上就要到雙11零點碍讨,很快就會迎來一波搶購,這波商品在23點集中的放入了緩存蒙秒,假設(shè)緩存一個小時勃黍,那么到了凌晨24點的時候,這批商品的緩存就都過期了晕讲。

而對這批商品的訪問查詢覆获,都落到了數(shù)據(jù)庫上马澈,對于數(shù)據(jù)庫而言,就會產(chǎn)生周期性的壓力波峰,對數(shù)據(jù)庫造成壓力,甚至壓垮數(shù)據(jù)庫答毫。

緩存雪崩的原理圖如下,當正常的情況下涤伐,key沒有大量失效的用戶訪問原理圖如下:

image

當某一時間點,key大量失效缨称,造成的緩存雪崩的原理圖如下:


image

對于緩存雪崩的解決方案有以下兩種:

  1. 搭建高可用的集群凝果,防止單機的redis宕機。

  2. 設(shè)置不同的過期時間睦尽,防止同一時間內(nèi)大量的key失效器净。

針對業(yè)務(wù)系統(tǒng),永遠都是具體情況具體分析当凡,沒有最好掌动,只有最合適。于緩存其它問題宁玫,緩存滿了和數(shù)據(jù)丟失等問題,我們后面繼續(xù)深入的學(xué)習(xí)柑晒。最后也提一下三個詞LRU欧瘪、RDB、AOF匙赞,通常我們采用LRU策略處理溢出佛掖,Redis的RDB和AOF持久化策略來保證一定情況下的數(shù)據(jù)安全。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涌庭,一起剝皮案震驚了整個濱河市芥被,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坐榆,老刑警劉巖拴魄,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異席镀,居然都是意外死亡匹中,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門豪诲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來顶捷,“玉大人,你說我怎么就攤上這事屎篱》辏” “怎么了葵蒂?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長重虑。 經(jīng)常有香客問我践付,道長,這世上最難降的妖魔是什么嚎尤? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任荔仁,我火速辦了婚禮,結(jié)果婚禮上芽死,老公的妹妹穿的比我還像新娘乏梁。我一直安慰自己,他們只是感情好关贵,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布遇骑。 她就那樣靜靜地躺著,像睡著了一般揖曾。 火紅的嫁衣襯著肌膚如雪落萎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天炭剪,我揣著相機與錄音练链,去河邊找鬼。 笑死奴拦,一個胖子當著我的面吹牛媒鼓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播错妖,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼绿鸣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了暂氯?” 一聲冷哼從身側(cè)響起潮模,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痴施,沒想到半個月后擎厢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡晾剖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年锉矢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片齿尽。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡沽损,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出循头,到底是詐尸還是另有隱情绵估,我是刑警寧澤炎疆,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站国裳,受9級特大地震影響形入,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缝左,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一亿遂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渺杉,春花似錦蛇数、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至倚评,卻和暖如春浦徊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背天梧。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工盔性, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呢岗。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓纯出,卻偏偏與公主長得像,于是被迫代替她去往敵國和親敷燎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351