日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

缓存穿透、缓存击穿和缓存雪崩

發布時間:2024/1/23 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 缓存穿透、缓存击穿和缓存雪崩 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

我們使用緩存的主要目是提升查詢速度和保護數據庫等稀缺資源不被占滿。

而緩存最常見的問題是緩存穿透、擊穿和雪崩,在高并發下這三種情況都會有大量請求落到數據庫,導致數據庫資源占滿,引起數據庫故障。今天我主要分享一下layering-cache緩存框架在這個三個問題上的實踐方案。

?

?

概念

?

緩存穿透

在高并發下,查詢一個不存在的值時,緩存不會被命中,導致大量請求直接落到數據庫上,如活動系統里面查詢一個不存在的活動。

?

緩存擊穿

在高并發下,對一個特定的值進行查詢,但是這個時候緩存正好過期了,緩存沒有命中,導致大量請求直接落到數據庫上,如活動系統里面查詢活動信息,但是在活動進行過程中活動緩存突然過期了。

?

緩存雪崩

在高并發下,大量的緩存key在同一時間失效,導致大量的請求落到數據庫上,如活動系統里面同時進行著非常多的活動,但是在某個時間點所有的活動緩存全部過期。

?

常見解決方案

  • 直接緩存NULL值

  • 限流

  • 緩存預熱

  • 分級緩存

  • 緩存永遠不過期

?

?

layering-cache實踐

在layering-cache里面結合了緩存NULL值,緩存預熱,限流、分級緩存和間接的實現"永不過期"等幾種方案來應對緩存穿透、擊穿和雪崩問題。

?

直接緩存NULL值

應對緩存穿透最有效的方法是直接緩存NULL值,但是緩存NULL的時間不能太長,否則NULL數據長時間得不到更新,也不能太短,否則達不到防止緩存擊穿的效果。

?

我在layering-cache對NULL值進行了特殊處理,一級緩存不允許存NULL值,二級緩存可以配置緩存是否允許存NULL值,如果配置可以允許存NULL值,框架還支持配置緩存非空值和NULL值之間的過期時間倍率,這使得我們能精準的控制每一個緩存的NULL值過期時間,控制粒度非常細。當NULL緩存過期我還可以使用限流,緩存預熱等手段來防止穿透。

?

示例:

  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
@Cacheable(value = "people", key = "#person.id", depict = "用戶信息緩存", firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS, isAllowNullValue = true, magnification = 10))public Person findOne(Person person) { Person p = personRepository.findOne(Example.of(person)); logger.info("為id、key為:" + p.getId() + "數據做了緩存"); return p;}

?

在這個例子里面isAllowNullValue = true表示允許緩存NULL值,magnification = 10表示NULL值和非NULL值之間的時間倍率是10,也就是說當緩存值為NULL值,二級緩存的有效時間將是1個小時。

?

限流

應對緩存穿透的常用方法之一是限流,常見的限流算法有滑動窗口,令牌桶算法和漏桶算法,或者直接使用隊列、加鎖等,在layering-cache里面我主要使用分布式鎖來做限流。

?

layering-cache數據讀取流程:?

數據讀取流程.jpg

?

下面是讀取數據的核心代碼:

?

  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
private <T> T executeCacheMethod(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_sync_lock"); // 同一個線程循環20次查詢緩存,每次等待20毫秒,如果還是沒有數據直接去執行被緩存的方法 for (int i = 0; i < RETRY_COUNT; i++) { try { // 先取緩存,如果有直接返回,沒有再去做拿鎖操作 Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey()); if (result != null) { logger.debug("redis緩存 key= {} 獲取到鎖后查詢查詢緩存命中,不需要執行被緩存的方法", redisCacheKey.getKey()); return (T) fromStoreValue(result); } // 獲取分布式鎖去后臺查詢數據 if (redisLock.lock()) { T t = loaderAndPutValue(redisCacheKey, valueLoader, true); logger.debug("redis緩存 key= {} 從數據庫獲取數據完畢,喚醒所有等待線程", redisCacheKey.getKey()); // 喚醒線程 container.signalAll(redisCacheKey.getKey()); return t; } // 線程等待 logger.debug("redis緩存 key= {} 從數據庫獲取數據未獲取到鎖,進入等待狀態,等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME); container.await(redisCacheKey.getKey(), WAIT_TIME); } catch (Exception e) { container.signalAll(redisCacheKey.getKey()); throw new LoaderCacheValueException(redisCacheKey.getKey(), e); } finally { redisLock.unlock(); } } logger.debug("redis緩存 key={} 等待{}次,共{}毫秒,任未獲取到緩存,直接去執行被緩存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME); return loaderAndPutValue(redisCacheKey, valueLoader, true);}

?

當需要加載緩存的時候,需要獲取到鎖才有權限到后臺去加載緩存數據,否則就會等待(同一個線程循環20次查詢緩存,每次等待20毫秒,如果還是沒有數據直接去執行被緩存的方法,這個主要是為了防止獲取到鎖并且去加載緩存的線程出問題,沒有返回而導致死鎖)。當獲取到鎖的線程執行完成會將獲取到的數據放到緩存中,并且喚醒所有等待線程。

?

這里需要注意一下讓線程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的時候,我發現當并發達到300左右,緩存一旦過期就會引起死鎖,原因是使用的是sleep方法來讓沒有獲取到鎖的線程等待,當等待的線程很多的時候會產生大量上下文切換,導致獲取到鎖的線程一直獲取不到cpu的執行權,導致死鎖。在layering-cache里面,我們使用的是LockSupport.parkNanos方法,它會釋放cpu資源, 因為我們使用的是redis分布式鎖,所以也不能使用wait-notify機制。

?

緩存預熱

有效應對緩存的擊穿和雪崩的方式之一是緩存預加載。

?

  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
@Cacheable(value = "people", key = "#person.id", depict = "用戶信息緩存", firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 2,timeUnit = TimeUnit.HOURS,))public Person findOne(Person person) { Person p = personRepository.findOne(Example.of(person)); logger.info("為id、key為:" + p.getId() + "數據做了緩存"); return p;}

?

在 layering-cache里面二級緩存會配置兩個時間,expireTime是緩存的過期時間,preloadTime 是緩存的刷新時間(預加載時間)。每次二級緩存被命中都會去檢查緩存的過去時間是否小于刷新時間,如果小于就會開啟一個異步線程預先去更新緩存,并將新的值放到緩存中,有效的保證了熱點數據**"永不過期"**。這里預先更新緩存也是需要加鎖的,并不是所有的線程都會落到庫上刷新緩存,如果沒有獲取到鎖就直接結束當前線程。

?

  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
/** * 刷新緩存數據 */private <T> void refreshCache(RedisCacheKey redisCacheKey, Callable<T> valueLoader, Object result) { Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); Long preload = preloadTime; // 允許緩存NULL值,則自動刷新時間也要除以倍數 boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null); if (flag) { preload = preload / getMagnification(); } if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) { // 判斷是否需要強制刷新在開啟刷新線程 if (!getForceRefresh()) { logger.debug("redis緩存 key={} 軟刷新緩存模式", redisCacheKey.getKey()); softRefresh(redisCacheKey); } else { logger.debug("redis緩存 key={} 強刷新緩存模式", redisCacheKey.getKey()); forceRefresh(redisCacheKey, valueLoader); } }
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
  • ?
/** * 硬刷新(執行被緩存的方法) * * @param redisCacheKey {@link RedisCacheKey} * @param valueLoader 數據加載器 */private <T> void forceRefresh(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { // 盡量少的去開啟線程,因為線程池是有限的 ThreadTaskUtils.run(() -> { // 加一個分布式鎖,只放一個請求去刷新緩存 Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_lock"); try { if (redisLock.lock()) { // 獲取鎖之后再判斷一下過期時間,看是否需要加載數據 Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) { // 加載數據并放到緩存 loaderAndPutValue(redisCacheKey, valueLoader, false); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } });}

?

在緩存總量和并發量都很大的時候,這個時候緩存如果同時失效,緩存預熱將是一個非常慢長的過程,就比如說服務重啟或新上線一個新的緩存。這個時候我們可以采用切流的方式,讓緩存慢慢預熱,如開始切10%流量,觀察沒有異常后,再切30%流量,觀察沒有異常后,再切60%流量,然后全量。這種方式雖然有點繁瑣,但是一旦遇到異常我們可以快速的切回流量,讓風險可控。

?

總結

總體來說layering-cache在緩存穿透、擊穿和雪崩上是以預防為主,補救為輔。而在應對緩存的這些問題上其實也沒有一個完全完美的方案,只有最適合自己業務系統的方案。目前如果直接使用layering-cache緩存框架已經基本能應對大部分的緩存問題了。

?

源碼

https://github.com/xiaolyuh/layering-cache

?

layering-cache

為監控而生的多級緩存框架 layering-cache這是我開源的一個多級緩存框架的實現,如果有興趣可以看一下

GitHub地址:https://github.com/xiaolyuh/layering-cache

?

作者:xiaolyuh

https://my.oschina.net/u/3748347/blog/2995017

總結

以上是生活随笔為你收集整理的缓存穿透、缓存击穿和缓存雪崩的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。