1. 程式人生 > 其它 >Redis---快取雪崩,快取穿透,快取擊穿的區別及解決方案

Redis---快取雪崩,快取穿透,快取擊穿的區別及解決方案

一、快取處理流程

前臺請求,後臺先從快取中取資料,取到直接返回結果,取不到時從資料庫中取,資料庫取到更新快取,並返回結果,資料庫也沒取到,那直接返回空結果。

二:快取雪崩

概念:當快取伺服器重啟或者大量快取集中在某一個時間段失效,這樣在失效的時候由於查詢資料量巨大,引起資料庫壓力過大甚至down機。

解決方案:

  1. 快取資料的過期時間在一個基礎的時間上加一個隨機值,防止同一時間大量資料過期現象發生。
  2. 如果快取資料庫是分散式部署,將熱點資料均勻分佈在不同快取資料庫中。
  3. 設定熱點資料永遠不過期。

三:快取穿透

概念:快取穿透是指快取和資料庫中都沒有的資料,而使用者不斷髮起請求,快取層和儲存層都不會命中,通常出於容錯的考慮,如果從儲存層查不到資料則不寫入快取層。

例如使用者不斷髮起為id為“-1”的資料或id為特別大不存在的資料。這時的使用者很可能是攻擊者,攻擊會導致資料庫壓力過大。這時可以在介面層增加校驗,如使用者鑑權校驗,id做基礎校驗,id<=0的直接攔截;

解決方案一:快取空物件

/**
 * 快取空物件:
 * 此種方式存在漏洞,不經過判斷就直接將Null物件存入到快取中,
 * 如果惡意製造很多不存在的id,那麼快取中的鍵值就會很多,惡意攻擊時,很可能會被打爆,所以需設定較短的過期時間。
 */
public Object getObjectInclNullById(Integer id) {
    // 從快取中獲取資料
    Object cacheValue = cache.get(id);
    
// 快取為空 if (cacheValue != null) { // 從資料庫中獲取 Object storageValue = storage.get(key); // 快取空物件 cache.set(key, storageValue); // 如果儲存資料為空,需要設定一個過期時間(300秒) if (storageValue == null) { // 必須設定過期時間,否則有被攻擊的風險 cache.expire(key, 60 * 5); }
return storageValue; } return cacheValue; }

快取空物件會有一個必須考慮的問題:

快取空物件的時候快取層中會存更多的鍵,需要更多的記憶體空間(如果是攻擊,問題更嚴重),比較有效的方法是針對這類資料設定一個較短的過期時間,讓其自動剔除。

解決方案二:布隆過濾器攔截

布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和刪除困難。如果想判斷一個元素是不是在一個集合裡,一般想到的是將集合中所有元素儲存起來,然後通過比較確定。連結串列、樹、散列表(又叫雜湊表,Hash table)等等資料結構都是這種思路。但是隨著集合中元素的增加,我們需要的儲存空間越來越大。同時檢索速度也越來越慢,上述三種結構的檢索時間複雜度分別為 O(n),O(log n),O(n/k)。

布隆過濾器的原理是,當一個元素被加入集合時,通過K個雜湊函式將這個元素對映成一個位數組中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。

示例:google guava包下有對布隆過濾器的封裝,BloomFilter。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterTest { // 初始化一個能夠容納10000個元素且容錯率為0.01布隆過濾器 private static final BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01); //初始化布隆過濾器 private static void initLegalIdsBloomFilter() { // 初始化10000個合法Id並加入到過濾器中 for (int legalId = 0; legalId < 10000; legalId++) { bloomFilter.put(legalId); } } //id是否合法有效,即是否在過濾器中 public static boolean validateIdInBloomFilter(Integer id) { return bloomFilter.mightContain(id); } public static void main(String[] args) { // 初始化過濾器 initLegalIdsBloomFilter(); // 誤判個數 int errorNum=0; // 驗證從10000個非法id是否有效 for (int id = 10000; id < 20000; id++) { if (validateIdInBloomFilter(id)){ // 誤判數 errorNum++; } } System.out.println("judge error num is : " + errorNum); } }

實現布隆過濾器攔截

設定過期時間,讓其自動過期失效,這種在很多時候不是最佳的實踐方案。

我們可以提前將真實正確的商品Id,在新增完成之後便加入到過濾器當中,每次再進行查詢時,先確認要查詢的Id是否在過濾器當中,如果不在,則說明Id為非法Id,則不需要進行後續的查詢步驟了。

/**
 * 防快取穿透的:布隆過濾器
 */
public Object getObjectByBloom(Integer id) {
    // 判斷是否為合法id
    if (!bloomFilter.mightContain(id)) {
        // 非法id,則不允許繼續查庫
        return null;
    } else {
        // 從快取中獲取資料
        Object cacheValue = cache.get(id);
        // 快取為空
        if (cacheValue == null) {
            // 從資料庫中獲取
            Object storageValue = storage.get(id);
            // 快取空物件
            cache.set(id, storageValue);
        }
        return cacheValue;
    }
} 

四:快取擊穿

概念:快取擊穿是指快取中沒有但資料庫中有的資料(一般是快取時間到期),這時由於併發使用者特別多,同時讀快取沒讀到資料,又同時去資料庫去取資料,引起資料庫壓力瞬間增大,造成過大壓力

補充:快取擊穿和快取雪崩的區別在於這裡針對某一key快取,而快取雪崩是很多key。

通常使用快取 + 過期時間的策略來幫助我們加速介面的訪問速度,減少了後端負載,同時保證功能的更新,一般情況下這種模式已經基本滿足要求了。但如下兩個問題如果同時出現,可能就會對系統造成致命的危害:其一:這個key是一個熱點key,其二是key的訪問量非常大快取的構建是需要一定時間的。(可能是一個複雜計算,例如複雜的sql、多次IO、多個依賴(各種介面)等等),於是就會出現一個致命問題:在快取失效的瞬間,有大量執行緒來構建快取(見下圖),造成後端負載加大,甚至可能會讓系統崩潰 。

解決方案:

  1. 設定熱點資料永遠不過期。
  2. 加互斥鎖,互斥鎖參考程式碼如下:
fight for future!