緩存穿透、擊穿、雪崩什麼的傻傻分不清楚?看了這篇文後,我明白了

對於緩存,你們確定都不陌生,不論是前端仍是服務端開發,緩存幾乎都是必不可少的優化方式之一。在實際生產環境中,緩存的使用規範也是一直備受重視的,若是使用的很差,很容易就遇到緩存擊穿、雪崩等嚴重異常情景,從而給系統帶來難以預料的災害。前端

爲了不緩存使用不當帶來的損失,咱們有必要了解每種異常產生的緣由和解決辦法,從而作出更好的預防措施。程序員

緩存穿透

而緩存穿透是指緩存和數據庫中都沒有的數據,這樣每次請求都會去查庫,不會查緩存,若是同一時間有大量請求進來的話,就會給數據庫形成巨大的查詢壓力,甚至擊垮db系統。web

好比說查詢id爲-1的商品,這樣的id在商品表裏確定不存在,若是沒作特殊處理的話,攻擊者很容易可讓系統奔潰,那咱們該如何避免這種狀況發生呢?面試

通常來講,緩存穿透經常使用的解決方案大概有兩種:redis

1、緩存空對象算法

當緩存和數據都查不到對應key的數據時,能夠將返回的空對象寫到緩存中,這樣下次請求該key時直接從緩存中查詢返回空對象,就不用走db了。固然,爲了不存儲過多空對象,一般會給空對象設置一個比較短的過時時間,就好比像這樣給key設置30秒的過時時間:數據庫

redisTemplate.opsForValue().set(key, null, 30, TimeUnit.SECONDS);

這種方法會存在兩個問題:數組

  • 若是有大量的key穿透,緩存空對象會佔用寶貴的內存空間。
  • 空對象的key設置了過時時間,這段時間內可能數據庫恰好有了該key的數據,從而致使數據不一致的狀況。

這種狀況下,咱們能夠用更好的解決方案,也就是布隆過濾器緩存

2、Bloom Filter服務器

布隆過濾器(Bloom Filter)是1970年由一個叫布隆的小夥子提出的,是一種由一個很長的二進制向量和一系列隨機映射函數構成的機率型數據結構,這種數據結構的空間效率很是高,能夠用於檢索集合中是否存在特定的元素。

設計思想

布隆過濾器由一個長度爲m比特的位數組(bit array)與k個哈希函數(hash function)組成的數據結構。原理是當一個元素被加入集合時,經過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,咱們只要看看這些點是否是都是1就大約知道集合中有沒有它了,也就是說,若是這些點有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。

至於說爲何都是1的狀況只是可能存在檢索元素,這是由於不一樣的元素計算的哈希值有可能同樣,會出現哈希碰撞,致使一個不存在的元素有可能對應的比特位爲1。

舉個例子:下圖是一個布隆過濾器,共有18個比特位,3個哈希函數。當查詢某個元素w時,經過三個哈希函數計算,發現有一個比特位的值爲0,能夠確定認爲該元素不在集合中。

優缺點

優勢:

  • 節省空間:不須要存儲數據自己,只須要存儲數據對應hash比特位
  • 時間複雜度低:基於哈希算法來查找元素,插入和查找的時間複雜度都爲O(k),k爲哈希函數的個數

缺點:

  • 準確率有誤:布隆過濾器判斷存在,可能出現元素不在集合中;判斷準確率取決於哈希函數的個數
  • 不能刪除元素:若是一個元素被刪除,可是卻不能從布隆過濾器中刪除,這樣進一步致使了不存在的元素也會顯示1的狀況。

適用場景

  • 爬蟲系統url去重
  • 垃圾郵件過濾
  • 黑名單

緩存擊穿

緩存擊穿從字面上看很容易讓人跟穿透搞混,這也是不少面試官喜歡埋坑的地方,固然,只要咱們對知識點了然於心的話,面試的時候也不會那麼被糊弄

簡單來講,緩存擊穿是指一個key很是熱點,在不停的扛着大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫,就好像堤壩忽然破了一個口,大量洪水洶涌而入。

當發生緩存擊穿的時候,數據庫的查詢壓力會倍增,致使大量的請求阻塞。

解決辦法也不難,既然是熱點key,那麼說明該key會一直被訪問,既然如此,咱們就不對這個key設置失效時間了,若是數據須要更新的話,咱們能夠後臺開啓一個異步線程,發現過時的key直接重寫緩存便可。

固然,這種解決方案只適用於不要求數據嚴格一致性的狀況,由於當後臺線程在構建緩存的時候,其餘的線程頗有可能也在讀取數據,這樣就會訪問到舊數據了。

若是要嚴格保證數據一致的話,能夠用互斥鎖

互斥鎖

互斥鎖就是說,當key失效的時候,讓一個線程讀取數據並構建到緩存中,其餘線程就先等待,直到緩存構建完後從新讀取緩存便可。

若是是單機系統,用JDK自己的同步工具Synchronized或ReentrantLock就能夠實現,但通常來講,都達到防止緩存擊穿的流量了誰還搞什麼單機系統,確定是分佈式高大上點啊,這種狀況咱們就能夠用分佈式鎖來作互斥效果。

爲了大家能更懂流程,做爲暖男的我仍是一如既往的給大家準備了僞代碼啦:

public String getData(String key){
    String data = redisTemplate.opsForValue().get(key);
    if (StringUtils.isNotEmpty(data)){
        return data;
    }
    String lockKey = this.getClass().getName() + ":" + key;
    RLock lock = redissonClient.getLock(lockKey);
    try {
        boolean boo = lock.tryLock(5, 5, TimeUnit.SECONDS);
        if (!boo) {
            // 休眠一下子,而後再請求
            Thread.sleep(200L);
            data = getData(key);
        }
        // 讀取數據庫的數據
        data = getDataByDB(key);
        if (StringUtils.isNotEmpty(data)){
            // 把數據構建到緩存中
            setDataToRedis(key,data);
        }
    } catch (InterruptedException e) {
        // 異常處理,記錄日誌或者拋異常什麼的
    }finally {
        if (lock != null && lock.isLocked()){
            lock.unlock();
        }
    }
    return data;
}

固然,採用互斥鎖的方案也是有缺陷的,當緩存失效的時候,同一時間只有一個線程讀數據庫而後回寫緩存,其餘線程都處於阻塞狀態。若是是高併發場景,大量線程阻塞勢必會下降吞吐量。這種狀況該如何處理呢?我只能說沒什麼設計是完美的,你又想數據一致,又想保證吞吐量,哪有那麼好的事,爲了系統能更加健全,必要的時候犧牲下性能也是能夠採起的措施,二者之間怎麼取捨要根據實際業務場景來決定,萬能的技術方案什麼的根本不存在。

緩存雪崩

緩存雪崩也是key失效後大量請求打到數據庫的異常狀況,不過,跟緩存擊穿不一樣的是,緩存擊穿由於指一個熱點key失效致使的狀況,而緩存雪崩是指緩存中大批量的數據同時過時,巨大的請求量直接落到db層,引發db壓力過大甚至宕機,這也符合字面上的「雪崩」說法。

解決方案

緩存雪崩的解決方案和擊穿的思路一致,能夠設置key不過時或者互斥鎖的方式。

除此以外,由於是預防大面積的key同時失效,能夠給不一樣的key過時時間加上隨機值,讓緩存失效的時間點儘可能均勻 ,這樣能夠保證數據不會在同一時間大面積失效

redisTemplate.opsForValue().set(Key, value, time + Math.random() * 1000, TimeUnit.SECONDS);  

同時還能夠結合主備緩存策略來讓互斥鎖的方式更加的可靠,

主緩存:有效期按照經驗值設置,設置爲主讀取的緩存,主緩存失效後從數據庫加載最新值。

備份緩存:有效期長,獲取鎖失敗時讀取的緩存,主緩存更新時須要同步更新備份緩存。

通常來講,上面三種緩存異常場景問的比較多,瞭解這幾種基本就夠了,但有些面試官可能喜歡劍走偏鋒,進一步延伸其餘的異常情景作詢問,以防萬一,咱們也加個菜,介紹下另外兩種常見緩存異常。

緩存預熱

緩存預熱就是系統上線後,先將相關的數據構建到緩存中,這樣就能夠避免用戶請求的時候直接查庫。

這部分預熱的數據主要取決於訪問量和數據量大小,若是數據的訪問量不大的話,那麼就不必作預熱,都沒什麼多少請求了,直接按正常的緩存讀取流程執行就好。

訪問量大的話,也要看數據的大小來作預熱措施。

  • 數據量不大的時候,工程啓動的時候進行加載緩存動做,這種數據通常能夠是電商首頁的運營位之類的信息;
  • 數據量大的時候,設置一個定時任務腳本,進行緩存的刷新;
  • 數據量太大的時候,優先保證熱點數據進行提早加載到緩存,而且確保訪問期間不能更改緩存,好比用定時器在秒殺活動前30分鐘就把商品信息之類的刷新到緩存,同時規定後臺運營人員不能在秒殺期間更改商品屬性。

緩存降級

緩存降級是指緩存失效或緩存服務器掛掉的狀況下,不去訪問數據庫,直接返回默認數據或訪問服務的內存數據。

在項目實戰中一般會將部分熱點數據緩存到服務的內存中,相似HashMap、Guava這樣的工具,一旦緩存出現異常,能夠直接使用服務的內存數據,從而避免數據庫遭受巨大壓力。

固然,這樣的操做對於業務是有損害的,分佈式系統中很容易就出現數據不一致的問題,因此,通常這種狀況下,咱們都優先保證從運維角度確保緩存服務器的高可用性,好比Redis的部署採用集羣方式,同時作好備份,總之,儘可能避免出現降級的影響。

最後

關於緩存的幾大異常處理咱們就講解到這了,雖然每種異常咱們都給出瞭解決的方案,但不是說這玩意直接套上就能用了。現實開發過程當中仍是要根據實際狀況來針對緩存作相應措施,好比用布隆過濾器預防緩存穿透雖然頗有效,但並不算特別經常使用,這年頭,防止惡意攻擊什麼的都是先在運維層面作限制,業務代碼層面更多的是對參數和數據作校驗。

若是每一個使用緩存的地方都要考慮的這麼複雜的話,那工做量無疑會更加繁雜,過分設計只會讓代碼維護起來也麻煩,並且實用性還不必定強,不必啊。程序員嘛,給本身增添煩惱的事情越少越好,畢竟咱們最大的敵人不是996,而是那珍貴的髮量啊。


更多精彩文章歡迎關注個人公衆號,掃描下方二維碼或者微信搜索鄙人薛某便可,回覆【電子書】還能獲取學習資料哦~~~咱們下期再見!

相關文章
相關標籤/搜索