redis內存溢出

Redis緩存雪崩、緩存穿透、熱點Key解決方案和分析

緩存穿透

緩存系統,按照KEY去查詢VALUE,當KEY對應的VALUE一定不存在的時候並對KEY併發請求量很大的時候,就會對後端造成很大的壓力。

(查詢一個必然不存在的數據。比如文章表,查詢一個不存在的id,每次都會訪問DB,如果有人惡意破壞,很可能直接對DB造成影響。)

由於緩存不命中,每次都要查詢持久層。從而失去緩存的意義。

 

解決方法:

1、緩存層緩存空值。 
–緩存太多空值,佔用更多空間。(優化:給個空值過期時間) 
–存儲層更新代碼了,緩存層還是空值。(優化:後臺設置時主動刪除空值,並緩存把值進去)

2、將數據庫中所有的查詢條件,放到布隆過濾器中。當一個查詢請求來臨的時候,先經過布隆過濾器進行檢查,如果請求存在這個條件中,那麼繼續執行,如果不在,直接丟棄。

 

備註:

    比如數據庫中有10000個條件,那麼布隆過濾器的容量size設置的要稍微比10000大一些,比如12000.

    對於誤判率的設置,根據實際項目,以及硬件設施來具體決定。但是一定不能設置爲0,並且誤判率設置的越小,哈希函數跟數組長度都會更多跟更長,那麼對硬件,內存中間的要求就會相應的高。

  private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.0001); 

    有了size跟誤判率,那麼布隆過濾器就會產生相應的哈希函數跟數組。

    綜上:我們可以利用布隆過濾器,將redis緩存擊穿控制在一個可容忍的範圍內。

 


緩存雪崩(緩存失效)

        如果緩存集中在一段時間內失效,發生大量的緩存穿透,所有的查詢都落在數據庫上,造成了緩存雪崩。

        緩存層宕掉後,流量會像奔逃的野牛一樣,打向後端存儲

    解決方法:

  1. 在緩存失效後,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
  2. 可以通過緩存reload機制,預先去更新緩存,再即將發生大併發訪問前手動觸發加載緩存
  3. 不同的key,設置不同的過期時間,讓緩存失效的時間點儘量均勻
  4. 做二級緩存,或者雙緩存策略。A1爲原始緩存,A2爲拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置爲短期,A2設置爲長期。

 

 

熱點key

      (1) 這個key是一個熱點key(例如一個重要的新聞,一個熱門的八卦新聞等等),所以這種key訪問量可能非常大。

      (2) 緩存的構建是需要一定時間的。(可能是一個複雜計算,例如複雜的sql、多次IO、多個依賴(各種接口)等等)

       於是就會出現一個致命問題:在緩存失效的瞬間,有大量線程來構建緩存(見下圖),造成後端負載加大,甚至可能會讓系統崩潰 。

    解決方法:

1. 使用互斥鎖(mutex key):這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其他線程等待構建緩存的線程執行完,重新從緩存獲取數據就可以了

2. "提前"使用互斥鎖(mutex key):在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設置到cache。然後再從數據庫加載數據並設置到cache中。

3. "永遠不過期":

 這裏的“永遠不過期”包含兩層意思:

    (1) 從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。

    (2) 從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value裏,如果發現要過期了,通過一個後臺的異步線程進行緩存的構建,也就是“邏輯”過期

4. 資源保護:可以做資源的隔離保護主線程池,如果把這個應用到緩存的構建也未嘗不可。

四種方案對比:

      作爲一個併發量較大的互聯網應用,我們的目標有3個:

      1. 加快用戶訪問速度,提高用戶體驗。

      2. 降低後端負載,保證系統平穩。

      3. 保證數據“儘可能”及時更新(要不要完全一致,取決於業務,而不是技術。)

      所以第二節中提到的四種方法,可以做如下比較,還是那就話:沒有最好,只有最合適。 

解決方案 優點 缺點
簡單分佈式鎖(Tim yang)

 1. 思路簡單

2. 保證一致性

1. 代碼複雜度增大

2. 存在死鎖的風險

3. 存在線程池阻塞的風險

加另外一個過期時間(Tim yang)  1. 保證一致性 同上 
不過期(本文)

1. 異步構建緩存,不會阻塞線程池

1. 不保證一致性。

2. 代碼複雜度增大(每個value都要維護一個timekey)。

3. 佔用一定的內存空間(每個value都要維護一個timekey)。

資源隔離組件hystrix(本文)

1. hystrix技術成熟,有效保證後端。

2. hystrix監控強大。

 

 

1. 部分訪問存在降級策略。 


總結

 

   1.  熱點key + 過期時間 + 複雜的構建緩存過程 => mutex key問題

   2. 構建緩存一個線程做就可以了。

   3. 四種解決方案:沒有最佳只有最合適。

分析和相關解決

 

 

什麼是緩存穿透

    一般的緩存系統,都是按照key值去緩存查詢,如果不存在對應的value,就應該去DB中查找 。這個時候,如果請求的併發量很大,就會對後端的DB系統造成很大的壓力。這就叫做緩存穿透。關鍵詞:緩存value爲空;併發量很大去訪問DB。

 

 

造成的原因

1.業務自身代碼或數據出現問題;2.一些惡意攻擊、爬蟲造成大量空的命中,此時會對數據庫造成很大壓力。

解決方法

1.設置布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,
從避免了對底層存儲系統的查詢壓力。
2. 如果一個查詢返回的數據爲空,不管是數據不存在還是系統故障,我們仍然把這個結果進行緩存,但是它的過期時間會很短
最長不超過5分鐘。


二、雪崩

1.什麼是雪崩

因爲緩存層承載了大量的請求,有效的保護了存儲 層,但是如果緩存由於某些原因,整體不能夠提供服務,於是所有的請求,就會到達存儲層,存儲層的調用量就會暴增,造成存儲層也會掛掉的情況。緩存雪崩的英文解釋是奔逃的野牛,指的是緩存層當掉之後,併發流量會像奔騰的野牛一樣,大量後端存儲。

存在這種問題的一個場景是:當緩存服務器重啓或者大量緩存集中在某一個時間段失效,這樣在失效的時候,大量數據會去直接訪問DB,此時給DB很大的壓力。

 

 

2.解決方法

(1)設置redis集羣和DB集羣的高可用,如果redis出現宕機情況,可以立即由別的機器頂替上來。這樣可以防止一部分的風險。

(2)使用互斥鎖

在緩存失效後,通過加鎖或者隊列來控制讀和寫數據庫的線程數量。比如:對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。單機的話,可以使用synchronized或者lock來解決,如果是分佈式環境,可以是用redis的setnx命令來解決。

(3)不同的key,可以設置不同的過期時間,讓緩存失效的時間點不一致,儘量達到平均分佈。

(4)永遠不過期

redis中設置永久不過期,這樣就保證了,不會出現熱點問題,也就是物理上不過期。

(5)資源保護

使用netflix的hystrix,可以做各種資源的線程池隔離,從而保護主線程池。

3.使用

四種方案,沒有最佳只有最合適, 根據自己項目情況使用不同的解決策略。
 

懂的越多,不會的也就越多,知識之路是不斷進取的

把redis作爲緩存使用已經是司空見慣,但是使用redis後也可能會碰到一系列的問題,尤其是數據量很大的時候,經典的幾個問題如下:

(一)緩存和數據庫間數據一致性問題
分佈式環境下(單機就不用說了)非常容易出現緩存和數據庫間的數據一致性問題,針對這一點的話,只能說,如果你的項目對緩存的要求是強一致性的,那麼請不要使用緩存。我們只能採取合適的策略來降低緩存和數據庫間數據不一致的概率,而無法保證兩者間的強一致性。合適的策略包括 合適的緩存更新策略,更新數據庫後要及時更新緩存、緩存失敗時增加重試機制,例如MQ模式的消息隊列。

(二)緩存擊穿問題
緩存擊穿表示惡意用戶模擬請求很多緩存中不存在的數據,由於緩存中都沒有,導致這些請求短時間內直接落在了數據庫上,導致數據庫異常。這個我們在實際項目就遇到了,有些搶購活動、秒殺活動的接口API被大量的惡意用戶刷,導致短時間內數據庫c超時了,好在數據庫是讀寫分離,同時也有進行接口限流,hold住了。

解決方案的話:

方案1、使用互斥鎖排隊

業界比價普遍的一種做法,即根據key獲取value值爲空時,鎖上,從數據庫中load數據後再釋放鎖。若其它線程獲取鎖失敗,則等待一段時間後重試。這裏要注意,分佈式環境中要使用分佈式鎖,單機的話用普通的鎖(synchronized、Lock)就夠了。

複製代碼

 1 public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
 2     // 通過key獲取value
 3     String value = redisService.get(key);
 4     if (StringUtil.isEmpty(value)) {
 5         // 分佈式鎖,詳細可以參考https://blog.csdn.net/fanrenxiang/article/details/79803037
 6         //封裝的tryDistributedLock包括setnx和expire兩個功能,在低版本的redis中不支持
 7         try {
 8             boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
 9             if (locked) {
10                 value = userService.getById(key);
11                 redisService.set(key, value);
12                 redisService.del(lockKey);
13                 return value;
14             } else {
15                 // 其它線程進來了沒獲取到鎖便等待50ms後重試
16                 Thread.sleep(50);
17                 getWithLock(key, jedis, lockKey, uniqueId, expireTime);
18             }
19         } catch (Exception e) {
20             log.error("getWithLock exception=" + e);
21             return value;
22         } finally {
23             redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
24         }
25     }
26     return value;
27 }

複製代碼

 

這樣做思路比較清晰,也從一定程度上減輕數據庫壓力,但是鎖機制使得邏輯的複雜度增加,吞吐量也降低了,有點治標不治本。

方案2、接口限流與熔斷、降級

重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時要降級準備,當接口中的某些服務不可用時候,進行熔斷,失敗快速返回機制。

方案3、布隆過濾器

bloomfilter就類似於一個hash set,用於快速判某個元素是否存在於集合中,其典型的應用場景就是快速判斷一個key是否存在於某容器,不存在就直接返回。布隆過濾器的關鍵就在於hash算法和容器大小,下面先來簡單的實現下看看效果,我這裏用guava實現的布隆過濾器:

複製代碼

 1 <dependencies> 
 2 <dependency> 
 3 <groupId>com.google.guava</groupId> 
 4 <artifactId>guava</artifactId> 
 5 <version>23.0</version> 
 6 </dependency> 
 7 </dependencies> 
 8 public class BloomFilterTest {
 9 
10 private static final int capacity = 1000000;
11 private static final int key = 999998;
12 
13 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
14 
15 static {
16 for (int i = 0; i < capacity; i++) {
17 bloomFilter.put(i);
18 }
19 }
20 
21 public static void main(String[] args) {
22 /*返回計算機最精確的時間,單位微妙*/
23 long start = System.nanoTime();
24 
25 if (bloomFilter.mightContain(key)) {
26 System.out.println("成功過濾到" + key);
27 }
28 long end = System.nanoTime();
29 System.out.println("布隆過濾器消耗時間:" + (end - start));
30 int sum = 0;
31 for (int i = capacity + 20000; i < capacity + 30000; i++) {
32 if (bloomFilter.mightContain(i)) {
33 sum = sum + 1;
34 }
35 }
36 System.out.println("錯判率爲:" + sum);
37 }
38 }

複製代碼

成功過濾到999998
布隆過濾器消耗時間:215518
錯判率爲:318
可以看到,100w個數據中只消耗了約0.2毫秒就匹配到了key,速度足夠快。然後模擬了1w個不存在於布隆過濾器中的key,匹配錯誤率爲318/10000,也就是說,出錯率大概爲3%,跟蹤下BloomFilter的源碼發現默認的容錯率就是0.03:

1 public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
2 return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
3 }

 

我們可調用BloomFilter的這個方法顯式的指定誤判率:

 

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我們斷點跟蹤下,誤判率爲0.02和默認的0.03時候的區別:

 

 

對比兩個出錯率可以發現,誤判率爲0.02時數組大小爲8142363,0.03時爲7298440,誤判率降低了0.01,BloomFilter維護的數組大小也減少了843923,可見BloomFilter默認的誤判率0.03是設計者權衡系統性能後得出的值。要注意的是,布隆過濾器不支持刪除操作。用在這邊解決緩存穿透問題就是:

複製代碼

 1 public String getByKey(String key) {
 2 // 通過key獲取value
 3 String value = redisService.get(key);
 4 if (StringUtil.isEmpty(value)) {
 5 if (bloomFilter.mightContain(key)) {
 6 value = userService.getById(key);
 7 redisService.set(key, value);
 8 return value;
 9 } else {
10 return null;
11 }
12 }
13 return value;
14 }

複製代碼

 


(三)緩存雪崩問題
緩存在同一時間內大量鍵過期(失效),接着來的一大波請求瞬間都落在了數據庫中導致連接異常。

解決方案:

方案1、也是像解決緩存穿透一樣加鎖排隊,實現同上;

方案2、建立備份緩存,緩存A和緩存B,A設置超時時間,B不設值超時時間,先從A讀緩存,A沒有讀B,並且更新A緩存和B緩存;

方案3、設置緩存超時時間的時候加上一個隨機的時間長度,比如這個緩存key的超時時間是固定的5分鐘加上隨機的2分鐘,醬紫可從一定程度上避免雪崩問題;

複製代碼

 1 public String getByKey(String keyA,String keyB) {
 2 String value = redisService.get(keyA);
 3 if (StringUtil.isEmpty(value)) {
 4 value = redisService.get(keyB);
 5 String newValue = getFromDbById();
 6 redisService.set(keyA,newValue,31, TimeUnit.DAYS);
 7 redisService.set(keyB,newValue);
 8 }
 9 return value;
10 }

複製代碼

 

(四)緩存併發問題
這裏的併發指的是多個redis的client同時set key引起的併發問題。其實redis自身就是單線程操作,多個client併發操作,按照先到先執行的原則,先到的先執行,其餘的阻塞。當然,另外的解決方案是把redis.set操作放在隊列中使其串行化,必須的一個一個執行,具體的代碼就不上了,當然加鎖也是可以的,至於爲什麼不用redis中的事務,留給各位看官自己思考探究。

一、緩存

 


Redis做緩存是最常見的應用場景。客戶端請求在緩存層命中就直接返回,如果miss就去讀取存儲層,存儲層讀取到就寫入緩存層,然後再返回到客戶端。
優點:
加速讀寫
降低後端負載
缺點:
數據的不一致性
代碼維護成本
運維成本

二、緩存穿透優化

 


然而緩存可能會遇到這種問題:請求cache拿不到數據,就會去存儲層拿,都拿不到時,返回空值(可能會返回大量空值)。或者代碼有問題,拿不到數據。就會一直請求數據。導致後端打崩。

優化方法:
1、緩存層緩存空值。
–緩存太多空值,佔用更多空間。(優化:給個空值過期時間)
–存儲層更新代碼了,緩存層還是空值。(優化:後臺設置時主動刪除空值,並緩存把值進去)

三、緩存雪崩優化

 


redis掛了,客戶端直接請求到數據庫裏面。數據庫負載非常高。甚至數據庫拖掛了。

優化方法:
1、保持緩存層服務器的高可用。
–監控、集羣、哨兵。當一個集羣裏面有一臺服務器有問題,讓哨兵踢出去。
2、依賴隔離組件爲後端限流並降級。
比如推薦服務中,如果個性化推薦服務不可用,可以降級爲熱點數據。
3、提前演練。
演練 緩存層crash後,應用以及後端的負載情況以及可能出現的問題。
對此做一些預案設定。

四、熱點key 重建優化:

 


A、B、C、D同時請求一個資源,不存在時都要去請求存儲層,有可能會拖掛。

優化方法:
1、互斥鎖:
只允許一個請求重建緩存。
其他請求等待緩存重建執行完,重新從緩存獲取數據即可。

 


2、用戶過期
–“物理”不過期
–邏輯設置過期時間(根據上一次更新時間,構建一個隊列,主動去更新)

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章