天天看點

緩存常見問題及解決方式

作者:搬山道猿
緩存常見問題及解決方式

緩存常見問題

由于引入緩存首先需要考慮的就是緩存更新的方式,之前在緩存更新的幾種模式中我們介紹過。除了這個問題還有一些常見的問題,整理出一個表格,如下圖所示:

緩存問題 産生原因 解決方案
緩存不一緻 同步更新失敗、異步更新 最終一緻
緩存穿透 惡意攻擊 空對象緩存、布隆過濾器
緩存擊穿 熱點key失效 互斥更新、随機退避、
緩存雪崩 緩存挂掉 快速失敗熔斷、主從模式、叢集模式、差異失效時間
大key 存儲value很大、集合資料過多、資料未清理 拆分key,清理key
熱點key 預期外的通路量陡增,如突然出現的爆款商品 對key進行rehash然後複制到不同叢集,使用讀寫分離架構

資料不一緻

一緻性問題

資料不一緻的問題,可以說隻要使用緩存,就要考慮如何面對這個問題。緩存不一緻産生的原因一般有兩方面:

  • 選擇緩存更新模式的不同造成的不一緻,例如[緩存更新的幾種模式]的Cache Aside不管是先更新db還是先删除或更新cache,在高并發的情況下都有可能造成不一緻的情況,隻是不同的更新方式造成不一緻的機率不一樣,盡可能的選擇造成不一緻機率最小的更新模式。
  • 系統問題導緻失敗造成的不一緻,在這裡就是如緩存服務的機器當機,網絡異常造成的更新失敗等。

解決方案

  • 采用強一緻性協定,很少使用。
  • 最終一緻性,在絕大部分場景中,特别是互聯場景下,大多是保證最終一緻性。 重試機制mq 更新資料庫,若這一步就失敗,更新事務失敗復原。 更新緩存失敗,将失敗的資料寫入mq 消費mq得到失敗的資料,重新删除緩存 訂閱資料庫binlog[參考MySQL複制原理及應用canal],解耦緩存更新過程。

緩存穿透

問題

産生這個問題的原因可能是外部的惡意攻擊,例如,對使用者資訊進行了緩存,但惡意攻擊者使用不存在的使用者id頻繁請求接口,導緻查詢緩存不命中,然後穿透 DB 查詢依然不命中。這時會有大量請求穿透緩存通路到 DB,增加資料庫壓力甚至導緻系統當機。

解決方案

  • 業務上做非法參數的校驗,盡量避免非法請求打到緩存。
  • 對不存在的使用者,在緩存中儲存一個空對象進行标記,防止相同 ID 再次通路 DB。不過有時這個方法并不能很好解決問題,可能導緻緩存中存儲大量無用資料。
  • 使用 BloomFilter 過濾器,BloomFilter 的特點是存在性檢測,如果 BloomFilter 中不存在,那麼資料一定不存在;如果 BloomFilter 中存在,實際資料也有可能會不存在。非常适合解決這類的問題。

布隆過濾器

下面簡單介紹下布隆過濾器,布隆過濾器内部維護一個bitArray(位數組), 開始所有資料全部置 0 。當一個元素過來時,能過多個哈希函數(hash1,hash2,hash3…)計算不同的在哈希值,并通過哈希值找到對應的bitArray下标處,将裡面的值 0 置為 1 。需要說明的是,布隆過濾器有一個誤判率的概念,誤判率越低,則數組越長,所占空間越大。誤判率越高則數組越小,所占的空間越小。

緩存常見問題及解決方式

以上圖為例,具體的寫入過程(如有3個hash函數): 假設集合裡面有3個元素{x, y, z},哈希函數的個數為3。首先将位數組進行初始化,将裡面每個位都設定為0。對于集合裡面的每一個元素,将元素依次通過3個哈希函數進行映射,每次映射都會産生一個哈希值,這個值對應位數組上面的一個點,然後将位數組對應的位置标記為1。

查詢a元素是否存在集合中的時候,同樣的方法将a通過哈希映射到位數組上的3個點。如果3個點的其中有一個點不為1,則可以判斷該元素一定不存在集合中。反之,如果3個點都為1,則該元素可能存在集合中。

注意:此處不能判斷該元素是否一定存在集合中,可能存在一定的誤判率。可以從圖中可以看到:假設某個元素通過映射對應下标為4,5,6這3個點。雖然這3個點都為1,但是很明顯這3個點是不同元素經過哈希得到的位置,是以這種情況說明元素雖然不在集合中,也可能對應的都是1,這是誤判率存在的原因。

布隆過濾器能确定一個值一定不存在,但是不能确定一個值一定存在。緩存穿透正好利用"布隆過濾器能确定一個值一定不存在",是以不存在計算誤差。

使用布隆過濾器解決緩存穿透

了解布隆過濾器原理後,我們用布隆過濾器解決緩存穿透問題就很簡單了,在緩存前加一層布隆過濾器,利用布隆過濾器bitset存儲結構存儲資料庫中所有值,查詢緩存前,先查詢布隆過濾器,若一定不存在就傳回。

方案對比:

方案 使用場景 使用成本
緩存空對象

1. 空資料量不大

2. 資料頻繁變化實時性高

1.代碼維護簡單

2.需要過多的緩存空間

3. 資料不一緻

過濾器

1.資料量比較大

2. 資料命中不高

3. 資料相對固定實時性低

1.代碼維護複雜

2.緩存空間占用少

緩存擊穿

問題

緩存擊穿,就是某個熱點資料失效時,很多請求這一時間都查不到緩存,然後全部請求并發打到了資料庫去查詢資料建構緩存,造成資料庫壓力非常大甚至當機。

解決方案

解決這個問題有如下辦法:

  • 使用互斥鎖更新,保證同一個程序中針對同一個資料不會并發請求到 DB,減小 DB 壓力。
java複制代碼 public Object getCache(final String key) {
    Object value = redis.get(key);
    //緩存值過期
    if (value == null) {    
        //加mutexKey的互斥鎖
        String mutexKey = mutexKey(key);
        if (redis.setnx(mutexKey, 1, time)) {  
            value = db.get(key);
            redis.set(key, value, time);
            redis.delete(mutexKey);
        } else {
            sleep(100); 
            return get(key);  
        }
    }
    return value;
}
           
  • 不給熱點資料設定過期時間,由背景異步更新緩存,或者在熱點資料準備要過期前,提前通知背景線程更新緩存以及重新設定過期時間;
方法 優點 缺點
互斥鎖 1.簡單易用2.一緻性保證 1.存線上程阻塞的風險2.資料庫通路的壓力轉到分布式鎖上來
異步更新 1.相比互斥鎖方案,降低線程阻塞的時間 1.代碼更複雜2.邏輯過期時間會占用一定的記憶體空間

緩存雪崩

問題

緩存雪崩。産生的原因是:

  • 大量請求同時打到DB上,比如大量key同時過期
  • 緩存服務挂掉,這時所有的請求都會穿透到 DB。

解決方案

  • 使用快速失敗的熔斷限流政策,減少 DB 瞬間壓力;
  • 使用主從模式和叢集模式來盡量保證緩存服務的高可用。
  • 針對多個熱點 key 同時失效的問題,可以在緩存時使用固定時間加上一個小的随機數,避免大量熱點 key 同一時刻失效。

大key及熱點key

問題

大key及熱點key的定義(不同公司根據實際情況定義不同):

名詞 解釋
大Key

通常以Key的大小和Key中成員的數量來綜合判定,例如:

1. Key本身的資料量過大:一個String類型的Key,它的值為5 MB。

2. Key中的成員數過多:一個ZSET類型的Key,它的成員數量為10,000個。

3. Key中成員的資料量過大:一個Hash類型的Key,它的成員數量雖然隻有1,000個但這些成員的Value(值)總大小為100 MB。

熱Key

通常以其接收到的Key被請求頻率來判定,例如:

1. QPS集中在特定的Key:Redis執行個體的總QPS(每秒查詢率)為10,000,而其中一個Key的每秒通路量達到了7,000。

2. 帶寬使用率集中在特定的Key:對一個擁有上千個成員且總大小為1 MB的HASH Key每秒發送大量的HGETALL操作請求。

3. CPU使用時間占比集中在特定的Key:對一個擁有數萬個成員的Key(ZSET類型)每秒發送大量的ZRANGE操作請求。

大key及熱點key的問題:

類别 說明
大Key

1. 用戶端執行指令的時長變慢。

2. Redis記憶體達到maxmemory參數定義的上限引發操作阻塞或重要的Key被逐出,甚至引發記憶體溢出(Out Of Memory)。

3. 叢集架構下,某個資料分片的記憶體使用率遠超其他資料分片,無法使資料分片的記憶體資源達到均衡。

4. 對大Key執行讀請求,會使Redis執行個體的帶寬使用率被占滿,導緻自身服務變慢,同時易波及相關的服務。

5. 對大Key執行删除操作,易造成主庫較長時間的阻塞,進而可能引發同步中斷或主從切換。

熱點Key

1. 占用大量的CPU資源,影響其他請求并導緻整體性能降低。

2. 叢集架構下,産生通路傾斜,即某個資料分片被大量通路,而其他資料分片處于空閑狀态,可能引起該資料分片的連接配接數被耗盡,新的連接配接建立請求被拒絕等問題。

3. 在搶購或秒殺場景下,可能因商品對應庫存Key的請求量過大,超出Redis處理能力造成超賣。

4. 熱Key的請求壓力數量超出Redis的承受能力易造成緩存擊穿,即大量請求将被直接指向後端的存儲層,導緻存儲通路量激增甚至當機,進而影響其他業務。

解決方案

大key

  1. 單key存儲value很大
  2. 可以把value對象拆成多份,使用multiGet,這樣做的意義在于減少操作在一個節點的壓力,分散到多個節點。
  3. 使用hash,每個filed存儲對象的各屬性。
  4. 集合存儲了過多的的值
  5. 将這些元素分拆。以hash為例,原先的正常存取流程是
  6. scss複制代碼
  7. hget(hashKey, field); hset(hashKey, field, value);
  8. 現在,固定一個桶的數量,比如 1000, 每次存取的時候,先本地進行rehash,确定了該field落在哪個key上。
  9. ini複制代碼
  10. newHashKey = hashKey + ( *hash*(field) % 1000); hset (newHashKey, field, value) ; hget(newHashKey, field);
  11. 定期删除過期大key

熱點key

  1. 在Redis叢集架構中對熱Key進行複制
  2. 在Redis叢集架構中,由于熱Key的遷移粒度問題,無法将請求分散至其他資料分片,導緻單個資料分片的壓力無法下降。此時,可以将對應熱Key進行rehash後複制并遷移至其他資料分片,例如将熱Key foo複制出3個内容完全一樣的Key并名為foo2、foo3、foo4,将這三個Key遷移到其他資料分片來解決單個資料分片的熱Key壓力。
  3. 該方案的缺點在于需要關聯修改代碼,同時帶來了資料一緻性的挑戰(由原來更新一個Key演變為需要更新多個Key),僅建議該方案用來解決臨時棘手的問題。
  4. 使用讀寫分離架構
  5. 如果熱Key的産生來自于讀請求,您可以将執行個體改造成讀寫分離架構來降低每個資料分片的讀請求壓力,甚至可以不斷地增加從節點。但是讀寫分離架構在增加業務代碼複雜度的同時,也會增加Redis叢集架構複雜度。不僅要為多個從節點提供轉發層(如Proxy,LVS等)來實作負載均衡,還要考慮從節點數量顯著增加後帶來故障率增加的問題。Redis叢集架構變更會為監控、運維、故障處理帶來了更大的挑戰。

繼續閱讀