第三節:Redis緩存雪崩、擊穿、穿透、雙寫一致性、並發競爭、熱點key重建優化、BigKey的優化 等解決方案


一. 緩存雪崩

1. 含義

 同一時刻,大量的緩存同時過期失效。

2. 產生原因和后果

(1). 原因:由於開發人員經驗不足或失誤,大量熱點緩存設置了統一的過期時間。

(2). 產生后果:恰逢秒殺高峰,緩存過期,瞬間海量的QPS(每秒查詢次數)直接打到DB上,如果系統架構沒有熔斷機制,直接將導致系統全線崩潰。

3. 處理方案

(1). 設置不同的緩存失效時間,比如可以在緩存過期時間后面加個隨機數,這樣就避免同一時刻緩存大量過期失效。

setRedis(key,value,time + Math.random() * 9999);

(2). 針對系統的一些熱點數據, 可以設置緩存永不過期。 (或者定時更新

(3). 設置二級緩存架構C1、C2,C1在前,C2在后,C1的緩存可以設置不同的過期時間,C2緩存與DB保持強一致性,實現數據同步。

PS:該二級緩存架構,同樣也適用於解決下面的緩存擊穿。

(4). 從架構層面來說:Redis做集群,將熱點數據分配在不同的master上,減輕單點壓力,同時master要對應多個slave,保證高可用;  系統架構要有快速熔斷策略,減輕系統的壓力。 

 

二. 緩存擊穿

1. 含義

 某熱點Key扛着大量的並發請求,當key失效的一瞬間,大量的QPS打到DB上,導致系統癱瘓。 

PS:緩存擊穿和緩存雪崩類似,擊穿是某些熱點key失效一瞬間大量請求打到DB上,緩存雪崩是指緩存面積失效導致大量請求打到DB上。所以二者的處理方案類似。

 

 

2. 處理方案

(1). 熱點key過期時間后加隨機數 。

(2). 熱點key緩存永不過期(但是value需要開個子線程去更新)

(3). 二級緩存架構策略。(詳見上面)

(4). 采用互斥鎖更新,保證同一進程針對相同的數據不會並發打到DB上,從而減輕DB的壓力。

(5). 緩存失效的時候隨機sleep一個很短的時間,再次查詢,如果失敗則執行更新操作。

 

三. 緩存穿透

1. 含義

 業務請求中數據緩存中沒有,DB中也沒有,導致類似請求直接跨過緩存,反復在DB中查詢,與此同時緩存也不會得到更新。

舉個例子:

 商品表中的id是自增,並且以id為緩存的key,商品庫存為value事先存在redis中。但此時過來的請求id均為負數,-1,-2,-3,緩存沒有,DB中也沒有,造成類似請求直接跨過緩存,打在DB上。

2.處理方案

(1). cache null策略:DB查詢的結果即使為null,也給緩存的value設置為null,同時可以設置一個較短的過期時間,這樣就避免不存在的數據跨過緩存直接打到DB上。

偽代碼思路分享:

Public String get(String key) {
  //從緩存中獲取數據
  String cacheValue = cache.get(key);
  //緩存為空
  if (StringUtils.isBlank(cacheValue)) {
     // 從DB中獲取
     String storageValue = db.get(key);
     cache.set(key, storageValue);
//如果存儲數據為空,需要設置一個過期時間(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 緩存非空 return cacheValue; } }

 

剖析:

 該方案不是並不是最佳方案,還是上面的例子,比如我用不同的id進行請求,例如 id=-1,-2,。。。。-10000,會導致緩存中存在大量的null,當數量達到一定值的時候,根據緩存淘汰策略,會導致正常的key失效。

(2). 布隆過濾器:

 事先把存在的key都放到redis的BloomFilter 過濾器中,他的用途就是存在性檢測,如果 BloomFilter 中不存在,那么數據一定不存在;如果 BloomFilter 中存在,實際數據也有可能會不存在。

剖析:

 布隆過濾器可能會誤判,當不影響整體,所以目前該方案是處理此類問題最佳方案。

詳見:https://www.cnblogs.com/yaopengfei/p/13928512.html

 

四. 雙寫一致性

1. 含義

 雙寫一致性的含義就是:保證緩存中的數據 和 DB中數據一致。

2. 單線程下的解決方案

 單線程下實際上就是指並發不大,或者說對 緩存和DB數據一致性要求不是很高的情況。

 該問題就是經典的:緩存+數據庫讀寫的模式,就是 Cache Aside Pattern

解決思路:

(1). 查詢的時候,先查緩存,緩存中有數據,直接返回;緩存中沒有數據,去查詢數據庫,然后更新緩存。

(2). 更新DB的后,刪除緩存。

剖析:

(1). 為什么更新DB后,是刪除緩存,而不是更新緩存呢?

 舉個例子,比如該DB更新的頻率很高,比如1min中內更新100次把,如果更新緩存,緩存也對應了更新了100次,但緩存在這一分鍾內根本沒被調用,或者說該緩存10min才可能會被查詢一次,那么頻繁更新緩存是不是就產生了很多不必要的開銷呢。

 所以我們這里的思路是:用到緩存的時候,才去計算緩存

(2). 該方案高並發場景下是否適用?

 不適用

 比如更新DB后,還有沒有來得及刪除緩存,別的請求就已經讀取到緩存的數據了,此時讀取的數據和DB中的實際的數據是不一致的。

3. 高並發下的解決方案

 使用內存隊列解決,把 讀請求 和 寫請求 都放到隊列中,按順序執行(即串行化的方式解決)。(要定義多個隊列,不同的商品放到不同的隊列中,換言之,同一個隊列中只有一類商品)

剖析:

 這種方案也有弊端,當並發量高了,隊列容易阻塞,這個隊列的位置,反而成了整個系統的瓶頸了,所以說100%完美的方案不存在,只有最適合的方案,沒有最完美的方案。

 

五. 並發競爭

1. 含義

 多個微服務系統要同時操作redis的同一個key,比如正確的順序是 A→B→C,A執行的時候,突然網絡抖動了一下,導致B,C先執行了,從而導致整個流程業務錯誤。

2. 解決方案

  引入分布式鎖(zookeeper 或 redis自身)

 每個系統在操作之前,都要先通過 Zookeeper 獲取分布式鎖,確保同一時間,只能有一個系統實例在操作這個個 Key,別系統都不允許讀和寫。

 

六. 熱點緩存key的重建優化

1. 背景

 開發人員使用“緩存+過期時間”的策略既可以加速數據讀寫, 又保證數據的定期更新, 這種模式基本能夠滿足絕大部分需求。 但是有兩個問題如果同時出現, 可能就會對應用造成致命的危害:

  (1). 當前key是一個熱點key(例如一個熱門的娛樂新聞),並發量非常大。

  (2). 重建緩存不能在短時間完成, 可能是一個復雜計算, 例如復雜的SQL、 多次IO、 多個依賴等。

 在緩存失效的瞬間, 有大量線程來重建緩存, 造成后端負載加大, 甚至可能會讓應用崩潰。

2. 解決方案

 要解決這個問題主要就是要避免大量線程同時重建緩存

 我們可以利用互斥鎖來解決,此方法只允許一個線程重建緩存, 其他線程等待重建緩存的線程執行完, 重新從緩存獲取數據即可。

代碼思路分享:

String get(String key) {
 // 從Redis中獲取數據
 String value = redis.get(key);
 // 如果value為空, 則開始重構緩存
 if (value == null) {
  // 只允許一個線程重建緩存, 使用nx, 並設置過期時間ex
  String mutexKey = "mutext:key:" + key;
  if (redis.set(mutexKey, "1", "ex 180", "nx")) {
    // 從數據源獲取數據
    value = db.get(key);
    // 回寫Redis, 並設置過期時間
    redis.setex(key, timeout, value);
    // 刪除key_mutex
    redis.delete(mutexKey);
  }
  else {
  //其它線程休息50ms,重寫遞歸獲取
  Thread.sleep(50);
  get(key);
  }
}
  return value;
}

 

七. BigKey的危害及優化

 1. 什么是BigKey

 在Redis中,一個字符串最大512MB,一個二級數據結構(例如hash、list、set、zset)可以存儲大約40億個(2^32-1)個元素,但實際中如果下面兩種情況,我就會認為它是bigkey。

 (1). 字符串類型:它的big體現在單個value值很大,一般認為超過10KB就是bigkey。

 (2). 非字符串類型:哈希、列表、集合、有序集合,它們的big體現在元素個數太多

 一般來說,string類型控制在10KB以內,hash、list、set、zset元素個數不要超過5000反例:一個包含200萬個元素的list。非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發del操作,造成阻塞)

2. BigKey的危害

 (1). 導致redis阻塞

 (2). 網絡擁塞

 bigkey也就意味着每次獲取要產生的網絡流量較大,假設一個bigkey為1MB,客戶端每秒訪問量為1000,那么每秒產生1000MB的流量,對於普通的千兆網卡(按照字節算是128MB/s)的服務器來說簡直是滅頂之災,而且一般服務器會采用單機多實例的方式來部署,也就是說一個bigkey

可能會對其他實例也造成影響,其后果不堪設想。

 (3). 過期刪除

 有個bigkey,它安分守己(只執行簡單的命令,例如hget、lpop、zscore等),但它設置了過期時間,當它過期后,會被刪除,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazy-expire yes),就會存在阻塞Redis的可能性

3. BigKey的產生

 一般來說,bigkey的產生都是由於程序設計不當,或者對於數據規模預料不清楚造成的,來看幾個例子:

 (1) 社交類:粉絲列表,如果某些明星或者大v不精心設計下,必是bigkey。

 (2) 統計類:例如按天存儲某項功能或者網站的用戶集合,除非沒幾個人用,否則必是bigkey。

 (3) 緩存類:將數據從數據庫load出來序列化放到Redis里,這個方式非常常用,但有兩個地方需注意:第一,是不是有必要把所有字段都緩存;第二,有沒有相關關聯的數據,有的同學為了圖方便把相關數據都存一個key下,產生bigkey。

4. BigKey的優化

(1). 拆

 big list: list1、list2、...listN

 big hash:可以將數據分段存儲,比如一個大的key,假設存了1百萬的用戶數據,可以拆分成200個key,每個key下面存放5000個用戶數據

(2). 合理采用數據結構

 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出來(例如有時候僅僅需要hmget,而不是hgetall),刪除也是一樣,盡量使用優雅的方式來處理.

反例:

set user:1:name tom
set user:1:age 19
set user:1:favor football

推薦hash存對象:

hmset user:1 name tom age 19 favor football

(3). 控制key的生命周期,redis不是垃圾桶。

 建議使用expire設置過期時間(條件允許可以打散過期時間,防止集中過期)。

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM