生活随笔
收集整理的這篇文章主要介紹了
缓存那些事
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
概要
緩存是現(xiàn)在系統(tǒng)中必不可少的模塊,并且已經(jīng)成為了高并發(fā)高性能架構(gòu)的一個關鍵組件。從硬件緩存、到軟件緩存;從底層的操作系統(tǒng)到上層的應用系統(tǒng),緩存無處不在,在我理解,要深入掌握這門技術(shù),需要先掌握緩存的思想。
緩存解決的問題
說白了,緩存就是計算機系統(tǒng)中最常見的空間換時間的思想的體現(xiàn),為的就是盡最大可能提升計算機軟件系統(tǒng)的性能。舉幾個例子如:
1、內(nèi)存中的數(shù)據(jù)需要放到CPU中去計算,不是當需要計算的時候再從內(nèi)存中一個數(shù)據(jù)一個數(shù)據(jù)的去取,而是有高速cpu緩存一次性保存很多數(shù)據(jù),用于提升內(nèi)存和cpu之間的數(shù)據(jù)交換。
2、普通Web應用,通常我們從數(shù)據(jù)庫獲取數(shù)據(jù),然后返回給瀏覽器進行展示,數(shù)據(jù)庫的數(shù)據(jù)到瀏覽器,之間經(jīng)歷我們的數(shù)據(jù)庫,后端web應用(服務器內(nèi)存),網(wǎng)絡,再到瀏覽器,用戶想要更快的獲取到數(shù)據(jù),那么就可以利用緩存,提前把數(shù)據(jù)放到web應用、甚至放到瀏覽器。
3、復雜的系統(tǒng) ,用戶獲取數(shù)據(jù)的路線可能是下面的樣子:
瀏覽器 》 CDN(內(nèi)容分發(fā)網(wǎng)絡) 》 代理層 》 緩存中間件
》 應用層 》
》應用層緩存|緩存中間件 》 數(shù)據(jù)庫緩存 》 數(shù)據(jù)庫
緩存存在的問題
數(shù)據(jù)一致性問題
從上面描述的兩個場景不難看出,緩存使用時,最明顯存在的問題就是數(shù)據(jù)實時性問題,可能用戶獲取到的數(shù)據(jù)不是我們最新的數(shù)據(jù),即緩存與數(shù)據(jù)庫數(shù)據(jù)一致性問題。
解決方案
1、當然我們可以采用完全串行化的方式(即保證緩存操作與數(shù)據(jù)庫操作的原子性)保證緩存與數(shù)據(jù)庫的數(shù)據(jù)一致性問題。但是這與我們緩存通常要解決的高并發(fā)下問題相違背。
2、下面簡單說下幾種方式,其實都不能保證強一致性,其中前面3中方式不推薦,推薦第4種并且詳細說明(需要了解詳細為什么的可以查看文章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ù)庫,考慮寫與寫之間的并發(fā),會有問題
b、先更新數(shù)據(jù)庫,再更新緩存,考慮寫與寫之間的并發(fā),會有問題
c、先刪除緩存,再更新數(shù)據(jù)庫,考慮讀寫之間的并發(fā),有問題
d、先更新數(shù)據(jù)庫,再刪除緩存,推薦,但也存在較小幾率有問題,比如,讀先來讀數(shù)據(jù),發(fā)現(xiàn)緩存沒有,從數(shù)據(jù)庫獲取了數(shù)據(jù),準備更新緩存,此時寫更新了數(shù)據(jù)庫,然后刪除了緩存完成了寫操作;此刻,讀線程最后再用舊數(shù)據(jù)更新了緩存,則導致緩存里的數(shù)據(jù)是舊數(shù)據(jù),與數(shù)據(jù)庫里的新數(shù)據(jù)不一致。這種情況只會出現(xiàn)緩存里沒有數(shù)據(jù)的情況下。通過設置過期時間或者下次再有數(shù)據(jù)更新時消除不一致。
3、阿里開源canal,mysql與redis之間的增量同步中間服務,詳細使用方式可以查看
https://blog.csdn.net/lyl0724/article/details/80528428
https://blog.csdn.net/weixin_40606441/article/details/79840205
緩存雪崩
問題出現(xiàn):
redis持久化淘汰
redis緩存過期失效
redis重啟、升級
導致緩存查不到,短時間內(nèi)如果來大量請求,可能對數(shù)據(jù)庫造成壓力。
1、采用數(shù)據(jù)庫連接池可以避免對數(shù)據(jù)庫造成連接壓力。但是壓力總量不變,只是數(shù)據(jù)庫層面限流了。
2、將壓力提前,所以需要在應用層、業(yè)務層限流,在查詢數(shù)據(jù)庫前添加限流器,進入方法,先拿緩存,拿不到就獲取semphere,拿到鎖的先查緩存,查不到再查數(shù)據(jù)庫,查到數(shù)據(jù)庫再更新緩存。容錯、限流、降級
緩存擊穿
問題出現(xiàn):
當頻繁訪問數(shù)據(jù)庫本身就不存在的數(shù)據(jù)時,不論訪問多少次,都不會在緩存中找到,這就繞過了緩存層,造成了緩存擊穿
問題如何解決:
1、查詢到數(shù)據(jù)庫中不存在就給redis插入空值,但是這個解決不了大量不存在ID的查詢,因為會造成redis存儲大量沒用的控制信息。
2、filter,先判斷是否存在,把所有存在的數(shù)據(jù)的key加載到內(nèi)存或者redis。就可以先判斷是否存在了。
3、方案2會造成空間大量浪費,所以繼續(xù)優(yōu)化,只用一個bit來表示某個key是否存在,引出布隆過濾器。
BloomFilter
布隆過濾器采用bit和hash的方式實現(xiàn),空間占用小,但是會有少量因為hash取模算法導致相同的slot位置而沖突導致的存在誤判(不存在的不會誤判),意思是判斷存在,其實可能不存在,和更新數(shù)據(jù)困難的問題。布隆過濾器需要不斷維護。
這個誤判很少,1、可以通過設置null值解決。2、通過多次hash減少誤判
redis三方模塊redis-bloom,可以通過在配置文件中配置loadModules引入該模塊的功能。
RedisBloomFilter
結(jié)合緩存雪崩里的邏輯:
進入方法,先用bloomfilter判斷是否存在,先拿緩存,拿不到就獲取semphere,拿到鎖的先查緩存,查不到再查數(shù)據(jù)庫,查到數(shù)據(jù)庫再更新緩存。
解決方案
如果要解決上面提到的緩存雪崩與緩存穿透問題,往往需要在用到緩存的業(yè)務代碼中增加大量的邏輯,導致原先簡單的業(yè)務代碼變得復雜,甚至難以維護,但是我們可以使用spring AOP實現(xiàn)自定義緩存注解優(yōu)雅的處理上訴過程
注意:
1、spring面向切面編程的方式
2、我們可以使用spring提供的spel表達式解析器
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
;Semaphore semaphore
= new Semaphore(30);@Pointcut("@annotation(com.study.cache.stampeding.annotations.CoustomCache)")public void cachePointcut() {}@Around("cachePointcut()")public Object
doCache(ProceedingJoinPoint joinPoint
) {Object value
= null
;CoustomCache cacheAnnotation
= findCoustomCache(joinPoint
);String cacheKey
= parseCacheKey(joinPoint
);String bloomFilterName
= cacheAnnotation
.bloomFilterName();boolean exists
= filter
.exists(bloomFilterName
, cacheKey
);if(! exists
) {logger
.warn(Thread
.currentThread().getName()+" 您需要的商品是不存在的+++++++++++++++++++++++++++");return "您需要的商品是不存在的";}value
= mainRedisTemplate
.opsForValue().get(cacheKey
);if (value
!= null
) {logger
.debug("從緩存中讀取到值:" + value
);return value
;}try {if(semaphore
.tryAcquire(5, TimeUnit
.SECONDS
)) {value
= mainRedisTemplate
.opsForValue().get(cacheKey
);if (value
!= null
) {logger
.debug("從緩存中讀取到值:" + value
);return value
;}value
= joinPoint
.proceed();final String v
= value
.toString();mainRedisTemplate
.execute((RedisCallback
<Boolean>) conn
-> {return conn
.setEx(cacheKey
.getBytes(), 120, v
.getBytes());});}else { value
= mainRedisTemplate
.opsForValue().get(cacheKey
);if(value
!= null
) {logger
.debug("等待后,再次從緩存獲得");return value
;}logger
.debug("服務降級——容錯處理");}} 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
;}private String
parseCacheKey(ProceedingJoinPoint joinPoint
) {CoustomCache cacheAnnotation
;String cacheKey
= null
;try {MethodSignature signature
= (MethodSignature
) joinPoint
.getSignature();Method method
= joinPoint
.getTarget().getClass().getMethod(signature
.getName(), signature
.getMethod().getParameterTypes());cacheAnnotation
= findCoustomCache(joinPoint
);String keyEl
= cacheAnnotation
.key();ExpressionParser parser
= new SpelExpressionParser();Expression expression
= parser
.parseExpression(keyEl
);EvaluationContext context
= new StandardEvaluationContext(); 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 {String
key();String
prefix();String
bloomFilterName();
}
c、使用
@CoustomCache(key
= "#goodsId", prefix
= "goodsStock-", bloomFilterName
= "goodsBloomFilter")public Object
queryStockByAnn(final String goodsId
) {return databaseService
.queryFromDatabase(goodsId
);}
總結(jié)
最近工作比較忙,把以前的筆記整理了下形成了此篇文章,很多地方?jīng)]有詳細深入與畫圖舉例,現(xiàn)在這打個標記,后續(xù)希望自己能夠沉下來做一個完成的中間件的總結(jié)。
總結(jié)
以上是生活随笔為你收集整理的缓存那些事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。