Spring Cache原理解析
一. 各個(gè)注解回顧
在看 Spring Cache 源碼時(shí),我們先看下回顧一下幾個(gè)重要的注解;
1. @Cacheable
在方法執(zhí)行前查看是否有緩存對(duì)應(yīng)的數(shù)據(jù),如果有直接返回?cái)?shù)據(jù),如果沒有則調(diào)用方法獲取數(shù)據(jù)返回,并緩存起來(lái);
- unless屬性:條件符合則不緩存,不符合則緩存;挺容易搞混的;
- sync屬性:是否使用異步,默認(rèn)是 false;在一個(gè)多線程的環(huán)境中,某些操作可能被相同的參數(shù)并發(fā)地調(diào)用,同一個(gè) value 值可能被多次計(jì)算(或多次訪問 db),這樣就達(dá)不到緩存的目的。針對(duì)這些可能高并發(fā)的操作,我們可以使用 sync 參數(shù)來(lái)告訴底層的緩存提供者將緩存的入口鎖住,這樣就只能有一個(gè)線程計(jì)算操作的結(jié)果值,而其它線程需要等待。當(dāng)值為true,相當(dāng)于同步可以有效的避免緩存擊穿的問題;
@Cacheable(cacheNames = "cache1", key = "#id")
public User getUserById(int id) {
User user = userMapper.selectById(id);
log.info("getUserById: {}", user);
return user;
}2. @CachePut
@CachePut ,在每次執(zhí)行方法時(shí),將執(zhí)行結(jié)果以鍵值對(duì)的形式存入指定緩存中;
@CachePut(cacheNames = "cache1", key = "#user.id", unless = "#result == null")
public User updateUser(User user) {
int count = userMapper.updateUser(user);
if (count == 0) {
log.info("update failed");
return null;
}
return user;
}3. @CacheEvict
@CacheEvict,Evict,表示驅(qū)逐,會(huì)在調(diào)用方法時(shí)從緩存中移除已存儲(chǔ)的數(shù)據(jù);
@CacheEvict(cacheNames = "cache1", key = "#id")
public int deleteUserById(int id) {
int result = userMapper.deleteUserById(id);
log.info("delete result: {}", result);
return result;
}@CacheEvict 中有一個(gè)屬性 beforeInvocation:是否在方法執(zhí)行前就清空,默認(rèn)是 false,表示在方法執(zhí)行后清空緩存;
清除操作默認(rèn)是在對(duì)應(yīng)方法成功執(zhí)行之后觸發(fā)的,即方法如果因?yàn)閽伋霎惓6茨艹晒Ψ祷貢r(shí)不會(huì)觸發(fā)清除操作。使用beforeInvocation = true 可以改變觸發(fā)清除操作的時(shí)間,當(dāng)我們指定該屬性值為 true 時(shí),SpringCache 會(huì)在調(diào)用該方法之前清除緩存中的指定元素;
4. @Caching
可以在一個(gè)方法或者類上同時(shí)指定多個(gè) SpringCache 相關(guān)的注解;
- 默認(rèn)情況下不允許同時(shí)兩個(gè) @CachePut 作用在方法上,此時(shí)需要用到 @Caching 進(jìn)行組合;
- @CachePut 和 @Cacheable 和 CacheEvict 三者是可以同時(shí)作用在方法上的;
@Caching(
put = {@CachePut(cacheNames = "cache1", key = "#user.id", unless = "#result == null"),
@CachePut(cacheNames = "cache1", key = "#user.username", unless = "#result == null")}
)
public User updateUser(User user) {
int count = userMapper.updateUser(user);
if (count == 0) {
log.info("update failed");
return null;
}
return user;
}二. Spring Cache
Spring Cache 注解對(duì)應(yīng)的 PointcutAdvsior 是 BeanFactoryCacheOperationSourceAdvisor,我們主要看它的 Advice,它的 Advice 是 CacheInterceptor;
CacheInterceptor 的處理邏輯和事務(wù)相關(guān)的 TransactionInterceptor 非常類似,下面我們對(duì) CacheInterceptor 進(jìn)行簡(jiǎn)單分析;
1. CacheInterceptor
CacheInterceptor 是一個(gè) Advice,它實(shí)現(xiàn)了 MethodInterceptor 接口,我們主要看它作為一個(gè) MethodInterceptor 的 invoke() 邏輯;
// ----------------------------------- CacheInterceptor -------------------------------------
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor {
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
// 采用函數(shù)對(duì)象的方式,把該函數(shù)對(duì)象傳給父類的 execute() 執(zhí)行
// 最終還是執(zhí)行 invocation.proceed()
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
// 這里對(duì)異常做了一次封裝,并拋出此異常
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
try {
// 1. 執(zhí)行父類的 execute()
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
// 拋出原始異常
throw th.getOriginal();
}
}
}主要還是執(zhí)行父類 CacheAspectSupport#execute(),我們看 CacheAspectSupport#execute() 做了啥;
// --------------------------------- CacheAspectSupport -----------------------------------
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
// 1. 獲取 CacheOperationSource
// 一般我們使用的都是注解類型的,所以會(huì)獲取到 AnnotationCacheOperationSource
// AnnotationCacheOperationSource 內(nèi)部有注解解析器,可以解析得到 CacheOperations
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
// 2. 根據(jù) AnnotationCacheOperationSource 解析得到 targetClass#method() 上的 CacheOperations
// CacheOperation 是一個(gè)抽象類,它的三個(gè)實(shí)現(xiàn)類分別對(duì)應(yīng) @Cacheable、@CachePut、@CacheEvict
// CacheableOperation、CachePutOperation、CacheEvictOperation
// 其實(shí)就是解析到目標(biāo)類目標(biāo)方法上的注解元信息 CacheOperations
Collection<CacheOperation> operations =
cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
// 3. CacheOperations 不為空,將 CacheOperations 等信息聚合為 CacheOperationContexts 對(duì)象
// CacheOperationContexts 類功能很強(qiáng)大,debug 的時(shí)候可以點(diǎn)進(jìn)去看看
// 每個(gè) CacheOperation 都會(huì)對(duì)應(yīng)一個(gè) CacheOperationContext,塞入 CacheOperationContexts
// 執(zhí)行重載的 execute()
return execute(invoker, method,
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invoker.invoke();
}我們看它重載的 execute(),這個(gè)是核心方法;
// --------------------------------- CacheAspectSupport -----------------------------------
private Object execute(CacheOperationInvoker invoker,
Method method,
CacheOperationContexts contexts) {
// 1. 如果方法需要 sync 的話,此處在執(zhí)行 cache.get() 時(shí)會(huì)加鎖
// 我們可以先不看此處內(nèi)部邏輯
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
}
catch (Cache.ValueRetrievalException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
else {
return invokeOperation(invoker);
}
}
// 2. 執(zhí)行早期的 CacheEvict
// 如果 @CacheEvict 的 beforeInvocation 屬性是 true 的話(默認(rèn)是 false)
// 會(huì)在方法執(zhí)行前就執(zhí)行 CacheEvict
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// 3. 如果有 CacheableOperation 的話,也就是有 @Cacheable 注解的話
// 先去緩存中取緩存值,并包裝為 Cache.ValueWrapper 對(duì)象
// 如果所有的 @Cacheable 都沒有緩存值,cacheHit 將為 null
// 反過(guò)來(lái)說(shuō),但凡有一個(gè) @Cacheable 有緩存值,cacheHit 將不為 null
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// 4. 創(chuàng)建一個(gè) List<CachePutRequest> 集合
// 如果 cacheHit == null,此時(shí)需要把所有 @Cacheable 中對(duì)應(yīng) key 都執(zhí)行 CachePut 操作
// 所以這里會(huì)收集 CacheableOperation,并作為 CachePutOperation 塞入到 List<CachePutRequest> 中
List<CachePutRequest> cachePutRequests = new ArrayList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// 5.1 如果緩存中的值不為空,且沒有 @CachePut
// 顯然返回值就是 cacheValue,并做一定的包裝
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// 5.2 其他情況,緩存中的值為空 || 有 @CachePut
// 執(zhí)行目標(biāo)方法,返回值作為新的 cacheValue,并做一定的包裝
// 這里執(zhí)行目標(biāo)方法時(shí)可能會(huì)出現(xiàn)異常,出現(xiàn)異常的情況下就不會(huì)執(zhí)行 CachePut 和 CacheEvict 操作了!??!
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// 6. 收集顯式聲明的 CachePutOperation,也就是有顯式聲明的 @CachePut
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// 7. 執(zhí)行 Cache.put(),處理 CachePut 和沒有緩存命中的 Cacheable
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// 8. 處理 @CacheEvict
// @CacheEvict 的 beforeInvocation 為 false 時(shí),在目標(biāo)方法執(zhí)行完才執(zhí)行 CacheEvict
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
// 返回值
return returnValue;
}我們簡(jiǎn)單看一下 processCacheEvicts()、findCachedItem()、cachePutRequest.apply(),他們分別對(duì)應(yīng)于對(duì) @CacheEvict、@Cacheable、@CachePut 的處理邏輯;
1.1 processCacheEvicts()
processCacheEvicts() 用于處理 @CacheEvict 注解;
// --------------------------------- CacheAspectSupport -----------------------------------
private void processCacheEvicts(Collection<CacheOperationContext> contexts,
boolean beforeInvocation, Object result) {
for (CacheOperationContext context : contexts) {
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
if (beforeInvocation == operation.isBeforeInvocation()
&& isConditionPassing(context, result)) {
performCacheEvict(context, operation, result);
}
}
}
// --------------------------------- CacheAspectSupport -----------------------------------
private void performCacheEvict(CacheOperationContext context,
CacheEvictOperation operation, Object result) {
Object key = null;
// 1. 針對(duì)每個(gè) @CacheEvict 注解中的每個(gè) cache 都執(zhí)行處理
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache, operation.isBeforeInvocation());
}
else {
if (key == null) {
// 2. 生成 cacheKey
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
// 3. 執(zhí)行 doEvict()
doEvict(cache, key, operation.isBeforeInvocation());
}
}
}1.2 findCachedItem()
findCachedItem() 用于處理 @Cacheable 注解;
// --------------------------------- CacheAspectSupport -----------------------------------
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
// 1. 遍歷所有的 CacheOperationContext
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
// 2. 如果命中了一個(gè) @Cacheable 緩存,直接返回緩存值
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key in cache(s) " + context.getCacheNames());
}
}
}
}
// 3. 都沒命中,返回 null
return null;
}
// --------------------------------- CacheAspectSupport -----------------------------------
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
// 遍歷 @Cacheable 中所有的 cache
// 有一個(gè) cache 有緩存值就返回該緩存值
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key found in cache '" + cache.getName());
}
return wrapper;
}
}
return null;
}1.3 cachePutRequest.apply()
cachePutRequest.apply() 用于處理 @CachePut 注解;
public void apply(@Nullable Object result) {
if (this.context.canPutToCache(result)) {
// 遍歷 @CachePut 中所有的 cache,執(zhí)行 doPut()
for (Cache cache : this.context.getCaches()) {
doPut(cache, this.key, result);
}
}
}三. 補(bǔ)充
1. 方法返回值為null,會(huì)緩存嗎
會(huì)緩存;
如果方法返回值為 null,此時(shí) Cache.ValueWrapper 值如下:由于此圖無(wú)法加載暫時(shí)不展示。
它是一個(gè) SimpleValueWrapper,value 值是 null;
但是如果將注解改成如下,就不會(huì)緩存 null 值;
// result == null 的話不進(jìn)行緩存 @Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")
2. @Cacheable注解sync=true的效果
在多線程環(huán)境下,某些操作可能使用相同參數(shù)同步調(diào)用(相同的key),默認(rèn)情況下,緩存不鎖定任何資源,可能導(dǎo)致多次計(jì)算,對(duì)數(shù)據(jù)庫(kù)造成訪問壓力。對(duì)于這些特定的情況,屬性 sync 可以指示底層將緩存鎖住,使只有一個(gè)線程可以進(jìn)入計(jì)算,而其他線程堵塞,直到返回結(jié)果更新到緩存中。
3. 注解可以重復(fù)標(biāo)注嗎
不同的注解可以標(biāo)注多個(gè),且都能生效;相同的注解不行,編譯器會(huì)直接報(bào)錯(cuò);如果需要相同注解標(biāo)注多個(gè)等更復(fù)雜的場(chǎng)景,可以使用 @Caching 注解組合注解;
@CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以標(biāo)注多個(gè)
//@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解標(biāo)注兩個(gè)是不行的 因?yàn)樗⒉皇茾Repeatable的
@Cacheable(cacheNames = "demoCache", key = "#id")
@Override
public Object getFromDB(Integer id) {
System.out.println("模擬去db查詢~~~" + id);
return "hello cache...";
}編譯器會(huì)直接報(bào)錯(cuò),此圖暫時(shí)不展示。
到此這篇關(guān)于Spring Cache原理的文章就介紹到這了,更多相關(guān)Spring Cache原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)樹形List與扁平List互轉(zhuǎn)的示例代碼
在平時(shí)的開發(fā)中,我們時(shí)常會(huì)遇到需要將"樹形List"與"扁平List"互轉(zhuǎn)的情況,本文為大家整理了Java實(shí)現(xiàn)樹形List與扁平List互轉(zhuǎn)的示例代碼,希望對(duì)大家有所幫助2023-05-05
如何用idea編寫并運(yùn)行第一個(gè)spark scala處理程序
詳細(xì)介紹了如何使用IntelliJ IDEA創(chuàng)建Scala項(xiàng)目,包括配置JDK和Scala SDK,添加Maven支持,編輯pom.xml,并創(chuàng)建及運(yùn)行Scala程序,這為Scala初學(xué)者提供了一個(gè)基礎(chǔ)的項(xiàng)目搭建和運(yùn)行指南2024-09-09
SpringBoot+Idea熱部署實(shí)現(xiàn)流程解析
這篇文章主要介紹了SpringBoot+Idea熱部署實(shí)現(xiàn)流程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
Java 入門圖形用戶界面設(shè)計(jì)之列表框JList
圖形界面(簡(jiǎn)稱GUI)是指采用圖形方式顯示的計(jì)算機(jī)操作用戶界面。與早期計(jì)算機(jī)使用的命令行界面相比,圖形界面對(duì)于用戶來(lái)說(shuō)在視覺上更易于接受,本篇精講Java語(yǔ)言中關(guān)于圖形用戶界面的列表框JList2022-02-02
Java線程和操作系統(tǒng)線程的關(guān)系解讀
這篇文章主要介紹了Java線程和操作系統(tǒng)線程的關(guān)系解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
Java中注解@JsonFormat與@DateTimeFormat的使用
從數(shù)據(jù)庫(kù)獲取時(shí)間傳到前端進(jìn)行展示的時(shí)候,我們有時(shí)候可能無(wú)法得到一個(gè)滿意的時(shí)間格式的時(shí)間日期,本文主要介紹了Java中注解@JsonFormat與@DateTimeFormat的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08

