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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

缓存那些事

發(fā)布時(shí)間:2023/12/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 缓存那些事 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

概要

緩存是現(xiàn)在系統(tǒng)中必不可少的模塊,并且已經(jīng)成為了高并發(fā)高性能架構(gòu)的一個(gè)關(guān)鍵組件。從硬件緩存、到軟件緩存;從底層的操作系統(tǒng)到上層的應(yīng)用系統(tǒng),緩存無處不在,在我理解,要深入掌握這門技術(shù),需要先掌握緩存的思想。

緩存解決的問題

說白了,緩存就是計(jì)算機(jī)系統(tǒng)中最常見的空間換時(shí)間的思想的體現(xiàn),為的就是盡最大可能提升計(jì)算機(jī)軟件系統(tǒng)的性能。舉幾個(gè)例子如:
1、內(nèi)存中的數(shù)據(jù)需要放到CPU中去計(jì)算,不是當(dāng)需要計(jì)算的時(shí)候再?gòu)膬?nèi)存中一個(gè)數(shù)據(jù)一個(gè)數(shù)據(jù)的去取,而是有高速cpu緩存一次性保存很多數(shù)據(jù),用于提升內(nèi)存和cpu之間的數(shù)據(jù)交換。
2、普通Web應(yīng)用,通常我們從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),然后返回給瀏覽器進(jìn)行展示,數(shù)據(jù)庫(kù)的數(shù)據(jù)到瀏覽器,之間經(jīng)歷我們的數(shù)據(jù)庫(kù),后端web應(yīng)用(服務(wù)器內(nèi)存),網(wǎng)絡(luò),再到瀏覽器,用戶想要更快的獲取到數(shù)據(jù),那么就可以利用緩存,提前把數(shù)據(jù)放到web應(yīng)用、甚至放到瀏覽器。
3、復(fù)雜的系統(tǒng) ,用戶獲取數(shù)據(jù)的路線可能是下面的樣子:
瀏覽器 》 CDN(內(nèi)容分發(fā)網(wǎng)絡(luò)) 》 代理層 》 緩存中間件
》 應(yīng)用層 》
》應(yīng)用層緩存|緩存中間件 》 數(shù)據(jù)庫(kù)緩存 》 數(shù)據(jù)庫(kù)

緩存存在的問題

數(shù)據(jù)一致性問題

從上面描述的兩個(gè)場(chǎng)景不難看出,緩存使用時(shí),最明顯存在的問題就是數(shù)據(jù)實(shí)時(shí)性問題,可能用戶獲取到的數(shù)據(jù)不是我們最新的數(shù)據(jù),即緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)一致性問題。

解決方案

1、當(dāng)然我們可以采用完全串行化的方式(即保證緩存操作與數(shù)據(jù)庫(kù)操作的原子性)保證緩存與數(shù)據(jù)庫(kù)的數(shù)據(jù)一致性問題。但是這與我們緩存通常要解決的高并發(fā)下問題相違背。
2、下面簡(jiǎn)單說下幾種方式,其實(shí)都不能保證強(qiáng)一致性,其中前面3中方式不推薦,推薦第4種并且詳細(xì)說明(需要了解詳細(xì)為什么的可以查看文章https://blog.csdn.net/chang384915878/article/details/86756463
https://blog.csdn.net/qq_27384769/article/details/79499373
https://blog.kido.site/2018/11/24/db-and-cache-preface/)
a、先更新緩存,再更新數(shù)據(jù)庫(kù),考慮寫與寫之間的并發(fā),會(huì)有問題
b、先更新數(shù)據(jù)庫(kù),再更新緩存,考慮寫與寫之間的并發(fā),會(huì)有問題
c、先刪除緩存,再更新數(shù)據(jù)庫(kù),考慮讀寫之間的并發(fā),有問題
d、先更新數(shù)據(jù)庫(kù),再刪除緩存,推薦,但也存在較小幾率有問題,比如,讀先來讀數(shù)據(jù),發(fā)現(xiàn)緩存沒有,從數(shù)據(jù)庫(kù)獲取了數(shù)據(jù),準(zhǔn)備更新緩存,此時(shí)寫更新了數(shù)據(jù)庫(kù),然后刪除了緩存完成了寫操作;此刻,讀線程最后再用舊數(shù)據(jù)更新了緩存,則導(dǎo)致緩存里的數(shù)據(jù)是舊數(shù)據(jù),與數(shù)據(jù)庫(kù)里的新數(shù)據(jù)不一致。這種情況只會(huì)出現(xiàn)緩存里沒有數(shù)據(jù)的情況下。通過設(shè)置過期時(shí)間或者下次再有數(shù)據(jù)更新時(shí)消除不一致。
3、阿里開源canal,mysql與redis之間的增量同步中間服務(wù),詳細(xì)使用方式可以查看
https://blog.csdn.net/lyl0724/article/details/80528428
https://blog.csdn.net/weixin_40606441/article/details/79840205

緩存雪崩

問題出現(xiàn):
redis持久化淘汰
redis緩存過期失效
redis重啟、升級(jí)
導(dǎo)致緩存查不到,短時(shí)間內(nèi)如果來大量請(qǐng)求,可能對(duì)數(shù)據(jù)庫(kù)造成壓力。
1、采用數(shù)據(jù)庫(kù)連接池可以避免對(duì)數(shù)據(jù)庫(kù)造成連接壓力。但是壓力總量不變,只是數(shù)據(jù)庫(kù)層面限流了。
2、將壓力提前,所以需要在應(yīng)用層、業(yè)務(wù)層限流,在查詢數(shù)據(jù)庫(kù)前添加限流器,進(jìn)入方法,先拿緩存,拿不到就獲取semphere,拿到鎖的先查緩存,查不到再查數(shù)據(jù)庫(kù),查到數(shù)據(jù)庫(kù)再更新緩存。容錯(cuò)、限流、降級(jí)

緩存擊穿

問題出現(xiàn):
當(dāng)頻繁訪問數(shù)據(jù)庫(kù)本身就不存在的數(shù)據(jù)時(shí),不論訪問多少次,都不會(huì)在緩存中找到,這就繞過了緩存層,造成了緩存擊穿
問題如何解決:
1、查詢到數(shù)據(jù)庫(kù)中不存在就給redis插入空值,但是這個(gè)解決不了大量不存在ID的查詢,因?yàn)闀?huì)造成redis存儲(chǔ)大量沒用的控制信息。
2、filter,先判斷是否存在,把所有存在的數(shù)據(jù)的key加載到內(nèi)存或者redis。就可以先判斷是否存在了。
3、方案2會(huì)造成空間大量浪費(fèi),所以繼續(xù)優(yōu)化,只用一個(gè)bit來表示某個(gè)key是否存在,引出布隆過濾器。
BloomFilter
布隆過濾器采用bit和hash的方式實(shí)現(xiàn),空間占用小,但是會(huì)有少量因?yàn)閔ash取模算法導(dǎo)致相同的slot位置而沖突導(dǎo)致的存在誤判(不存在的不會(huì)誤判),意思是判斷存在,其實(shí)可能不存在,和更新數(shù)據(jù)困難的問題。布隆過濾器需要不斷維護(hù)。
這個(gè)誤判很少,1、可以通過設(shè)置null值解決。2、通過多次hash減少誤判

redis三方模塊redis-bloom,可以通過在配置文件中配置loadModules引入該模塊的功能。
RedisBloomFilter

結(jié)合緩存雪崩里的邏輯:
進(jìn)入方法,先用bloomfilter判斷是否存在,先拿緩存,拿不到就獲取semphere,拿到鎖的先查緩存,查不到再查數(shù)據(jù)庫(kù),查到數(shù)據(jù)庫(kù)再更新緩存。

解決方案

如果要解決上面提到的緩存雪崩與緩存穿透問題,往往需要在用到緩存的業(yè)務(wù)代碼中增加大量的邏輯,導(dǎo)致原先簡(jiǎn)單的業(yè)務(wù)代碼變得復(fù)雜,甚至難以維護(hù),但是我們可以使用spring AOP實(shí)現(xiàn)自定義緩存注解優(yōu)雅的處理上訴過程
注意:
1、spring面向切面編程的方式
2、我們可以使用spring提供的spel表達(dá)式解析器
SpelExpressionParser
借用網(wǎng)易云老師的代碼:
a、核心切面類

package com.study.cache.stampeding.annotations;import java.lang.reflect.Method; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit;import javax.annotation.Resource;import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component;import com.study.cache.stampeding.bloom.RedisBloomFilter;@Component @Aspect public class CoustomCacheAspect {private Logger logger = LoggerFactory.getLogger(this.getClass());@Resource(name = "mainRedisTemplate") StringRedisTemplate mainRedisTemplate;@AutowiredRedisBloomFilter filter;// 數(shù)據(jù)庫(kù)限流,根據(jù)數(shù)據(jù)庫(kù)連接數(shù)來定義大小Semaphore semaphore = new Semaphore(30);@Pointcut("@annotation(com.study.cache.stampeding.annotations.CoustomCache)")public void cachePointcut() {}// 定義相應(yīng)的事件@Around("cachePointcut()")public Object doCache(ProceedingJoinPoint joinPoint) {Object value = null;CoustomCache cacheAnnotation = findCoustomCache(joinPoint);// 解析緩存KeyString cacheKey = parseCacheKey(joinPoint);// 在緩存之前去進(jìn)行過濾String bloomFilterName = cacheAnnotation.bloomFilterName();boolean exists = filter.exists(bloomFilterName, cacheKey);if(! exists) {logger.warn(Thread.currentThread().getName()+" 您需要的商品是不存在的+++++++++++++++++++++++++++");return "您需要的商品是不存在的";}// 1、 判定緩存中是否存在value = mainRedisTemplate.opsForValue().get(cacheKey);if (value != null) {logger.debug("從緩存中讀取到值:" + value);return value;}// 訪問數(shù)據(jù)庫(kù)進(jìn)行限流try {if(semaphore.tryAcquire(5, TimeUnit.SECONDS)) {value = mainRedisTemplate.opsForValue().get(cacheKey);if (value != null) {logger.debug("從緩存中讀取到值:" + value);return value;}// 交給服務(wù)層方法實(shí)現(xiàn),從數(shù)據(jù)庫(kù)獲取value = joinPoint.proceed();// 塞到緩存,過期時(shí)間10Sfinal String v = value.toString();mainRedisTemplate.execute((RedisCallback<Boolean>) conn -> {return conn.setEx(cacheKey.getBytes(), 120, v.getBytes());});}else { // semaphore.tryAcquire(5, TimeUnit.SECONDS) 超時(shí)怎么辦?// 再去獲取一遍緩存,說不定已經(jīng)有請(qǐng)求構(gòu)建好了緩存。value = mainRedisTemplate.opsForValue().get(cacheKey);if(value != null) {logger.debug("等待后,再次從緩存獲得");return value;}// 緩存尚未構(gòu)建好,進(jìn)行服務(wù)降級(jí),容錯(cuò)// 友好的提示,對(duì)不起,票已售空、11.11 提示稍后付款;客官您慢些;// 不斷降低我們的預(yù)期目標(biāo), 外星人、小黑、華為、小米logger.debug("服務(wù)降級(jí)——容錯(cuò)處理");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} catch (Throwable e) {logger.error(e.getMessage(), e);}finally {try {semaphore.acquire();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}return value;}private CoustomCache findCoustomCache(ProceedingJoinPoint joinPoint) {CoustomCache cacheAnnotation;try {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());cacheAnnotation = method.getAnnotation(CoustomCache.class);return cacheAnnotation;} catch (NoSuchMethodException e) {e.printStackTrace();} catch (SecurityException e) {e.printStackTrace();}return null;}/*** 獲取緩存Key* @param joinPoint* @return*/private String parseCacheKey(ProceedingJoinPoint joinPoint) {CoustomCache cacheAnnotation;// 解析String cacheKey = null;try {// 0-1、 當(dāng)前方法上注解的內(nèi)容MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());cacheAnnotation = findCoustomCache(joinPoint);String keyEl = cacheAnnotation.key();// 0-2、 前提條件:拿到作為key的依據(jù) - 解析springEL表達(dá)式// 創(chuàng)建解析器ExpressionParser parser = new SpelExpressionParser();Expression expression = parser.parseExpression(keyEl);EvaluationContext context = new StandardEvaluationContext(); // 參數(shù)// 添加參數(shù)Object[] args = joinPoint.getArgs();DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();String[] parameterNames = discover.getParameterNames(method);for (int i = 0; i < parameterNames.length; i++) {context.setVariable(parameterNames[i], args[i].toString());}String key = expression.getValue(context).toString();cacheKey = cacheAnnotation.prefix() == null ? "" : cacheAnnotation.prefix() + key;} catch (ParseException e) {e.printStackTrace();} catch (EvaluationException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (SecurityException e) {e.printStackTrace();}return cacheKey;}}

b、注解類

package com.study.cache.stampeding.annotations;import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/*** 自定義的緩存注解*/ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CoustomCache {/*** key的規(guī)則,可以使用springEL表達(dá)式,可以使用方法執(zhí)行的一些參數(shù)*/String key();/*** 緩存key的前綴* @return*/String prefix();/*** 采用布隆過濾器的名稱* @return*/String bloomFilterName(); }

c、使用

@CoustomCache(key = "#goodsId", prefix = "goodsStock-", bloomFilterName = "goodsBloomFilter")public Object queryStockByAnn(final String goodsId) {// CRUD,只需要關(guān)系業(yè)務(wù)代碼,交給碼農(nóng)去做return databaseService.queryFromDatabase(goodsId);}

總結(jié)

最近工作比較忙,把以前的筆記整理了下形成了此篇文章,很多地方?jīng)]有詳細(xì)深入與畫圖舉例,現(xiàn)在這打個(gè)標(biāo)記,后續(xù)希望自己能夠沉下來做一個(gè)完成的中間件的總結(jié)。

總結(jié)

以上是生活随笔為你收集整理的缓存那些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。