Java - 深度學習 - 緩存穿透、緩存擊穿、緩存雪崩

前言

本文整理依稀唉,緩存的三大高併發情況下暴露出來的問題,也算是掃盲吧

三大現象

現象 說明
緩存穿透 黑客或自身問題,大量請求根本不存在的key,導致每次都進行數據庫查詢
緩存雪崩 假設redis在高峯期能抗住5k/s的請求,如果redis突然間全盤宕機,5k打到數據庫上,當然掛了
緩存擊穿 當某個key非常熱點處於集中式訪問的情況,當key失效瞬間,大量請求擊穿緩存。直接請求數據庫

緩存穿透

緩存穿透是指查詢一個根本不存在的數據, 緩存層和存儲層都不會命中, 通常出於容錯的考慮, 如果從存儲層查不到數據則不寫入緩存層。那麼將導致不存在的數據每次請求都要到存儲層去查詢, 失去了緩存保護後端存儲的意義。

造成緩存穿透的基本原因有兩種:

  1. 自身業務代碼BUG 或者 數據出現問題
  2. 惡意攻擊、爬蟲等會造成大量空命中

解決方法,主流的主要存在兩種:

  1. 緩存空對象
// 1. redis中查找
String cacheValue = redisCache.get(key);
if (StringUtils.isBlank(cacheValue)  && !"UNKNOWN".equals.(cacheValue)){
  // 2. 未命中數據庫中查找
  String storageValue = db.get(key);
  if(StringUtils.isBlank(storageValue)){
    // 3. 未命中,設置過期時間,避免惡意攻擊,導致每次都進db
    redisCache.set(key,"UNKNOWN",3000);
  }else{
    redisCache.set(key,storageValue);
  }
  return storageValue;
}
return value;
  1. 使用guvua的布隆過濾器,特點:某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>28.1-jre</version>
</dependency>
// 期望存入的數據個數1000,期望的誤差率0.001
BloomFilter<String>bloomFilter =
  BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf‐8")), 1000, 0.001);
//把所有數據存入布隆過濾器
void init(){
	for (String key: keys) {
			bloomFilter.put(key);
	}
}

String get(String key){
	// 從布隆過濾器這一級緩存判斷下key是否存在
	Boolean exist = bloomFilter.mightContain(key);
	if(!exist){
		return "";
	}
	// 從緩存中獲取數據
	String cacheValue = cache.get(key);
	// 緩存爲空
	if (StringUtils.isBlank(cacheValue)) {
    // 從存儲中獲取
    String storageValue = storage.get(key);
    cache.set(key, storageValue);
    // 如果存儲數據爲空, 需要設置一個過期時間(300秒)
    if (storageValue == null) {
      cache.expire(key, 60 * 5);
    }
		return storageValue;
	}else{
		// 緩存非空
		return cacheValue;
	}
}

緩存擊穿

某個key非常熱點,訪問非常頻繁,此時由於key失效 or 大批量key失效,大量請求同時穿透直達數據庫,可能會造成數據庫瞬間壓力過大直接掛掉。

解決方案

  1. 避免大批量key同時失效,緩存過期時間設置爲同一時間段內的不同時間。

    int expireTime = new Random().nextInt(300) + 300;
    redisCache.set(key,value,expireTime);
    
  2. 若緩存的數據基本不會更新,則可將熱點數據設置爲永不過期

    redisCache.set(key,value);
    
  3. 若緩存的數據更新不是很頻繁,並且緩存刷新的過程耗時較少時,可以採用分佈式鎖

    // 從緩存中獲取數據
    String cacheValue = cache.get(key);
    // 緩存爲空
    if (StringUtils.isBlank(cacheValue)) {
      // 獲得鎖
      interProcessMutex.acquire();
      // double-check
      cacheValue = cache.get(key);
      if (StringUtils.isBlank(cacheValue)) {
          // database - 商品列表
          cacheValue = getGoods(orderNoPath, curatorFramework);
          // 設置緩存
          redisCache.set(key,cacheValue);
      }
      // 釋放
      interProcessMutex.release();
    }
    return cacheValue;
    
  4. 若數據更新頻繁時,並且緩存刷新的過程較耗時,那麼才用定時器方案(也就是在緩存到期時,在定時器種完成刷新)

    @Scheduled(fixedRate = 3000)
    public void check() {
      String value = db.get(key);
      redisCache.set(key,value,2000);
    }
    

緩存雪崩

當大量請求來臨時緩存層支撐不住或宕掉後,流量就全部達到數據庫上, 於是大量請求都會達到存儲層, 存儲層的調用量會暴增, 造成存儲層也會級聯宕機的情況,這也就是雪崩效應。

解決方法

  1. 保證緩存層服務高可用性,比如使用Redis Sentinel或Redis Cluster
  2. 本地 ehcache + Hystrix 限流&降級組件,避免數據庫被打死
  3. redis持久化,一旦重啓,自動從磁盤上加載數據,快速恢復緩存數據

好處

  1. Redis採用集羣模式後,高可用性不容易宕機等
  2. 因爲限流最多隻能運行合理的量到達數據庫,並不會導致數據庫瞬間崩潰
  3. 未通過的請求將會進入降級策略,這時我們只需返回 友情提示/默認值/空白頁/警告 等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章