成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

緩存穿透、緩存擊穿和緩存雪崩實(shí)踐

存儲(chǔ) 存儲(chǔ)軟件
我們使用緩存的主要目是提升查詢速度和保護(hù)數(shù)據(jù)庫等稀缺資源不被占滿。而緩存最常見的問題是緩存穿透、擊穿和雪崩,在高并發(fā)下這三種情況都會(huì)有大量請(qǐng)求落到數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫資源占滿,引起數(shù)據(jù)庫故障。

[[281365]]

我們使用緩存的主要目是提升查詢速度和保護(hù)數(shù)據(jù)庫等稀缺資源不被占滿。而緩存最常見的問題是緩存穿透、擊穿和雪崩,在高并發(fā)下這三種情況都會(huì)有大量請(qǐng)求落到數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫資源占滿,引起數(shù)據(jù)庫故障。今天我主要分享一下layering-cache緩存框架在這個(gè)三個(gè)問題上的實(shí)踐方案。

概念

緩存穿透

在高并發(fā)下,查詢一個(gè)不存在的值時(shí),緩存不會(huì)被命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫上,如活動(dòng)系統(tǒng)里面查詢一個(gè)不存在的活動(dòng)。

緩存擊穿

在高并發(fā)下,對(duì)一個(gè)特定的值進(jìn)行查詢,但是這個(gè)時(shí)候緩存正好過期了,緩存沒有命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫上,如活動(dòng)系統(tǒng)里面查詢活動(dòng)信息,但是在活動(dòng)進(jìn)行過程中活動(dòng)緩存突然過期了。

緩存雪崩

在高并發(fā)下,大量的緩存key在同一時(shí)間失效,導(dǎo)致大量的請(qǐng)求落到數(shù)據(jù)庫上,如活動(dòng)系統(tǒng)里面同時(shí)進(jìn)行著非常多的活動(dòng),但是在某個(gè)時(shí)間點(diǎn)所有的活動(dòng)緩存全部過期。

常見解決方案

  • 直接緩存NULL值
  • 限流
  • 緩存預(yù)熱
  • 分級(jí)緩存
  • 緩存永遠(yuǎn)不過期

layering-cache實(shí)踐

在layering-cache里面結(jié)合了緩存NULL值,緩存預(yù)熱,限流、分級(jí)緩存和間接的實(shí)現(xiàn)"永不過期"等幾種方案來應(yīng)對(duì)緩存穿透、擊穿和雪崩問題。

直接緩存NULL值

應(yīng)對(duì)緩存穿透最有效的方法是直接緩存NULL值,但是緩存NULL的時(shí)間不能太長,否則NULL數(shù)據(jù)長時(shí)間得不到更新,也不能太短,否則達(dá)不到防止緩存擊穿的效果。

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

示例:

  1. @Cacheable(value = "people"key = "#person.id", depict = "用戶信息緩存"
  2.         firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), 
  3.         secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS, 
  4.                 isAllowNullValue = true, magnification = 10)) 
  5. public Person findOne(Person person) { 
  6.     Person p = personRepository.findOne(Example.of(person)); 
  7.     logger.info("為id、key為:" + p.getId() + "數(shù)據(jù)做了緩存"); 
  8.     return p; 

在這個(gè)例子里面isAllowNullValue = true表示允許換存NULL值,magnification = 10表示NULL值和非NULL值之間的時(shí)間倍率是10,也就是說當(dāng)緩存值為NULL是,二級(jí)緩存的有效時(shí)間將是1個(gè)小時(shí)。

限流

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

layering-cache數(shù)據(jù)讀取流程:

 

數(shù)據(jù)讀取流程.jpg

下面是讀取數(shù)據(jù)的核心代碼:

  1. private <T> T executeCacheMethod(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { 
  2.     Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_sync_lock"); 
  3.     // 同一個(gè)線程循環(huán)20次查詢緩存,每次等待20毫秒,如果還是沒有數(shù)據(jù)直接去執(zhí)行被緩存的方法 
  4.     for (int i = 0; i < RETRY_COUNT; i++) { 
  5.         try { 
  6.             // 先取緩存,如果有直接返回,沒有再去做拿鎖操作 
  7.             Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey()); 
  8.             if (result != null) { 
  9.                 logger.debug("redis緩存 key= {} 獲取到鎖后查詢查詢緩存命中,不需要執(zhí)行被緩存的方法", redisCacheKey.getKey()); 
  10.                 return (T) fromStoreValue(result); 
  11.             } 
  12.  
  13.  
  14.             // 獲取分布式鎖去后臺(tái)查詢數(shù)據(jù) 
  15.             if (redisLock.lock()) { 
  16.                 T t = loaderAndPutValue(redisCacheKey, valueLoader, true); 
  17.                 logger.debug("redis緩存 key= {} 從數(shù)據(jù)庫獲取數(shù)據(jù)完畢,喚醒所有等待線程", redisCacheKey.getKey()); 
  18.                 // 喚醒線程 
  19.                 container.signalAll(redisCacheKey.getKey()); 
  20.                 return t; 
  21.             } 
  22.             // 線程等待 
  23.             logger.debug("redis緩存 key= {} 從數(shù)據(jù)庫獲取數(shù)據(jù)未獲取到鎖,進(jìn)入等待狀態(tài),等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME); 
  24.             container.await(redisCacheKey.getKey(), WAIT_TIME); 
  25.         } catch (Exception e) { 
  26.             container.signalAll(redisCacheKey.getKey()); 
  27.             throw new LoaderCacheValueException(redisCacheKey.getKey(), e); 
  28.         } finally { 
  29.             redisLock.unlock(); 
  30.         } 
  31.     } 
  32.     logger.debug("redis緩存 key={} 等待{}次,共{}毫秒,任未獲取到緩存,直接去執(zhí)行被緩存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME); 
  33.     return loaderAndPutValue(redisCacheKey, valueLoader, true); 

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

這里需要注意一下讓線程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的時(shí)候,我發(fā)現(xiàn)當(dāng)并發(fā)達(dá)到300左右,緩存一旦過期就會(huì)引起死鎖,原因是使用的是sleep方法來讓沒有獲取到鎖的線程等待,當(dāng)?shù)却木€程很多的時(shí)候會(huì)產(chǎn)生大量上下文切換,導(dǎo)致獲取到鎖的線程一直獲取不到cpu的執(zhí)行權(quán),導(dǎo)致死鎖。在layering-cache里面,我們使用的是LockSupport.parkNanos方法,它會(huì)釋放cpu資源, 因?yàn)槲覀兪褂玫氖莚edis分布式鎖,所以也不能使用wait-notify機(jī)制。

緩存預(yù)熱

有效應(yīng)對(duì)緩存的擊穿和雪崩的方式之一是緩存預(yù)加載。

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

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

  1. /** 
  2.  * 刷新緩存數(shù)據(jù) 
  3.  */ 
  4. private <T> void refreshCache(RedisCacheKey redisCacheKey, Callable<T> valueLoader, Object result) { 
  5.     Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 
  6.     Long preload = preloadTime; 
  7.     // 允許緩存NULL值,則自動(dòng)刷新時(shí)間也要除以倍數(shù) 
  8.     boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null); 
  9.     if (flag) { 
  10.         preload = preload / getMagnification(); 
  11.     } 
  12.     if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) { 
  13.         // 判斷是否需要強(qiáng)制刷新在開啟刷新線程 
  14.         if (!getForceRefresh()) { 
  15.             logger.debug("redis緩存 key={} 軟刷新緩存模式", redisCacheKey.getKey()); 
  16.             softRefresh(redisCacheKey); 
  17.         } else { 
  18.             logger.debug("redis緩存 key={} 強(qiáng)刷新緩存模式", redisCacheKey.getKey()); 
  19.             forceRefresh(redisCacheKey, valueLoader); 
  20.         } 
  21.     } 
  1. /** 
  2.  * 硬刷新(執(zhí)行被緩存的方法) 
  3.  * 
  4.  * @param redisCacheKey {@link RedisCacheKey} 
  5.  * @param valueLoader   數(shù)據(jù)加載器 
  6.  */ 
  7. private <T> void forceRefresh(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { 
  8.     // 盡量少的去開啟線程,因?yàn)榫€程池是有限的 
  9.     ThreadTaskUtils.run(() -> { 
  10.         // 加一個(gè)分布式鎖,只放一個(gè)請(qǐng)求去刷新緩存 
  11.         Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_lock"); 
  12.         try { 
  13.             if (redisLock.lock()) { 
  14.                 // 獲取鎖之后再判斷一下過期時(shí)間,看是否需要加載數(shù)據(jù) 
  15.                 Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 
  16.                 if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) { 
  17.                     // 加載數(shù)據(jù)并放到緩存 
  18.                     loaderAndPutValue(redisCacheKey, valueLoader, false); 
  19.                 } 
  20.             } 
  21.         } catch (Exception e) { 
  22.             logger.error(e.getMessage(), e); 
  23.         } finally { 
  24.             redisLock.unlock(); 
  25.         } 
  26.     }); 

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

總結(jié)

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

源碼

https://github.com/wyh-chenfeng/layering-cache

layering-cache

為監(jiān)控而生的多級(jí)緩存框架 layering-cache這是我開源的一個(gè)多級(jí)緩存框架的實(shí)現(xiàn),如果有興趣可以看一下。

作者:xiaolyuh

原文地址:https://my.oschina.net/u/3748347/blog/2995017

責(zé)任編輯:武曉燕 來源: 博客園
相關(guān)推薦

2023-03-10 13:33:00

緩存穿透緩存擊穿緩存雪崩

2019-10-12 14:19:05

Redis數(shù)據(jù)庫緩存

2021-12-25 22:28:27

緩存穿透緩存擊穿緩存雪崩

2021-06-05 09:01:01

Redis緩存雪崩緩存穿透

2020-03-16 14:57:24

Redis面試雪崩

2022-05-27 07:57:20

緩存穿透緩存雪崩緩存擊穿

2022-03-08 00:07:51

緩存雪崩數(shù)據(jù)庫

2022-11-18 14:34:28

2023-11-10 14:58:03

2024-03-12 10:44:42

2023-04-14 07:34:19

2023-12-06 13:38:00

Redis緩存穿透緩存擊穿

2020-10-13 07:44:40

緩存雪崩 穿透

2020-12-28 12:37:36

緩存擊穿穿透

2020-03-05 09:09:18

緩存原因方案

2020-10-23 10:46:03

緩存雪崩擊穿

2022-07-11 07:36:36

緩存緩存雪崩緩存擊穿

2024-04-07 00:00:02

Redis雪崩緩存

2024-04-18 11:43:28

緩存數(shù)據(jù)庫Redis

2025-03-03 07:00:00

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 欧美三区在线观看 | 欧美一级三级在线观看 | 久久国产区 | 免费一区 | 日韩高清中文字幕 | 亚洲人一区 | 免费一区二区三区 | 一级做受毛片免费大片 | 欧美日韩一区二区三区不卡视频 | 成人精品鲁一区一区二区 | 中文字幕精 | 午夜视频一区二区三区 | 羞羞网站免费 | 在线免费观看日本视频 | 成人久久久 | 久久精品一区 | 操操日| 久久爱一区 | www国产亚洲精品 | 四虎影音 | 伊人久久大香线 | 日本免费在线 | 欧美精品一区二区三区在线播放 | 久久精品性视频 | 国产在线精品一区二区三区 | 久综合 | 久久99精品久久久久久噜噜 | 国产视频中文字幕在线观看 | 中文字幕成人av | 亚洲精品视频免费看 | 日韩综合 | 亚洲国产成人av好男人在线观看 | 欧美高清视频一区 | 黄a网站| 欧美中文一区 | 亚洲美女天堂网 | 精品少妇一区二区三区在线播放 | 91国产视频在线 | 久久久视 | 国产精品成人一区二区三区 | 欧美视频一区二区三区 |