Spring Cache原理解析
一. 各個注解回顧
在看 Spring Cache 源碼時,我們先看下回顧一下幾個重要的注解;
1. @Cacheable
在方法執(zhí)行前查看是否有緩存對應的數(shù)據(jù),如果有直接返回數(shù)據(jù),如果沒有則調(diào)用方法獲取數(shù)據(jù)返回,并緩存起來;
- unless屬性:條件符合則不緩存,不符合則緩存;挺容易搞混的;
- sync屬性:是否使用異步,默認是 false;在一個多線程的環(huán)境中,某些操作可能被相同的參數(shù)并發(fā)地調(diào)用,同一個 value 值可能被多次計算(或多次訪問 db),這樣就達不到緩存的目的。針對這些可能高并發(fā)的操作,我們可以使用 sync 參數(shù)來告訴底層的緩存提供者將緩存的入口鎖住,這樣就只能有一個線程計算操作的結(jié)果值,而其它線程需要等待。當值為true,相當于同步可以有效的避免緩存擊穿的問題;
@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í)行方法時,將執(zhí)行結(jié)果以鍵值對的形式存入指定緩存中;
@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ū)逐,會在調(diào)用方法時從緩存中移除已存儲的數(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 中有一個屬性 beforeInvocation:是否在方法執(zhí)行前就清空,默認是 false,表示在方法執(zhí)行后清空緩存;
清除操作默認是在對應方法成功執(zhí)行之后觸發(fā)的,即方法如果因為拋出異常而未能成功返回時不會觸發(fā)清除操作。使用beforeInvocation = true 可以改變觸發(fā)清除操作的時間,當我們指定該屬性值為 true 時,SpringCache 會在調(diào)用該方法之前清除緩存中的指定元素;
4. @Caching
可以在一個方法或者類上同時指定多個 SpringCache 相關的注解;
- 默認情況下不允許同時兩個 @CachePut 作用在方法上,此時需要用到 @Caching 進行組合;
- @CachePut 和 @Cacheable 和 CacheEvict 三者是可以同時作用在方法上的;
@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 注解對應的 PointcutAdvsior 是 BeanFactoryCacheOperationSourceAdvisor,我們主要看它的 Advice,它的 Advice 是 CacheInterceptor;
CacheInterceptor 的處理邏輯和事務相關的 TransactionInterceptor 非常類似,下面我們對 CacheInterceptor 進行簡單分析;
1. CacheInterceptor
CacheInterceptor 是一個 Advice,它實現(xiàn)了 MethodInterceptor 接口,我們主要看它作為一個 MethodInterceptor 的 invoke() 邏輯;
// ----------------------------------- CacheInterceptor ------------------------------------- public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor { @Override public Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); // 采用函數(shù)對象的方式,把該函數(shù)對象傳給父類的 execute() 執(zhí)行 // 最終還是執(zhí)行 invocation.proceed() CacheOperationInvoker aopAllianceInvoker = () -> { try { return invocation.proceed(); } catch (Throwable ex) { // 這里對異常做了一次封裝,并拋出此異常 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 // 一般我們使用的都是注解類型的,所以會獲取到 AnnotationCacheOperationSource // AnnotationCacheOperationSource 內(nèi)部有注解解析器,可以解析得到 CacheOperations CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { // 2. 根據(jù) AnnotationCacheOperationSource 解析得到 targetClass#method() 上的 CacheOperations // CacheOperation 是一個抽象類,它的三個實現(xiàn)類分別對應 @Cacheable、@CachePut、@CacheEvict // CacheableOperation、CachePutOperation、CacheEvictOperation // 其實就是解析到目標類目標方法上的注解元信息 CacheOperations Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { // 3. CacheOperations 不為空,將 CacheOperations 等信息聚合為 CacheOperationContexts 對象 // CacheOperationContexts 類功能很強大,debug 的時候可以點進去看看 // 每個 CacheOperation 都會對應一個 CacheOperationContext,塞入 CacheOperationContexts // 執(zhí)行重載的 execute() return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass)); } } } return invoker.invoke(); }
我們看它重載的 execute(),這個是核心方法;
// --------------------------------- CacheAspectSupport ----------------------------------- private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { // 1. 如果方法需要 sync 的話,此處在執(zhí)行 cache.get() 時會加鎖 // 我們可以先不看此處內(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 的話(默認是 false) // 會在方法執(zhí)行前就執(zhí)行 CacheEvict processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); // 3. 如果有 CacheableOperation 的話,也就是有 @Cacheable 注解的話 // 先去緩存中取緩存值,并包裝為 Cache.ValueWrapper 對象 // 如果所有的 @Cacheable 都沒有緩存值,cacheHit 將為 null // 反過來說,但凡有一個 @Cacheable 有緩存值,cacheHit 將不為 null Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); // 4. 創(chuàng)建一個 List<CachePutRequest> 集合 // 如果 cacheHit == null,此時需要把所有 @Cacheable 中對應 key 都執(zhí)行 CachePut 操作 // 所以這里會收集 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í)行目標方法,返回值作為新的 cacheValue,并做一定的包裝 // 這里執(zhí)行目標方法時可能會出現(xiàn)異常,出現(xiàn)異常的情況下就不會執(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 時,在目標方法執(zhí)行完才執(zhí)行 CacheEvict processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); // 返回值 return returnValue; }
我們簡單看一下 processCacheEvicts()、findCachedItem()、cachePutRequest.apply(),他們分別對應于對 @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. 針對每個 @CacheEvict 注解中的每個 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. 如果命中了一個 @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 // 有一個 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); } } }
三. 補充
1. 方法返回值為null,會緩存嗎
會緩存;
如果方法返回值為 null,此時 Cache.ValueWrapper 值如下:由于此圖無法加載暫時不展示。
它是一個 SimpleValueWrapper,value 值是 null;
但是如果將注解改成如下,就不會緩存 null 值;
// result == null 的話不進行緩存 @Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")
2. @Cacheable注解sync=true的效果
在多線程環(huán)境下,某些操作可能使用相同參數(shù)同步調(diào)用(相同的key),默認情況下,緩存不鎖定任何資源,可能導致多次計算,對數(shù)據(jù)庫造成訪問壓力。對于這些特定的情況,屬性 sync 可以指示底層將緩存鎖住,使只有一個線程可以進入計算,而其他線程堵塞,直到返回結(jié)果更新到緩存中。
3. 注解可以重復標注嗎
不同的注解可以標注多個,且都能生效;相同的注解不行,編譯器會直接報錯;如果需要相同注解標注多個等更復雜的場景,可以使用 @Caching 注解組合注解;
@CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以標注多個 //@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解標注兩個是不行的 因為它并不是@Repeatable的 @Cacheable(cacheNames = "demoCache", key = "#id") @Override public Object getFromDB(Integer id) { System.out.println("模擬去db查詢~~~" + id); return "hello cache..."; }
編譯器會直接報錯,此圖暫時不展示。
到此這篇關于Spring Cache原理的文章就介紹到這了,更多相關Spring Cache原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java實現(xiàn)樹形List與扁平List互轉(zhuǎn)的示例代碼
在平時的開發(fā)中,我們時常會遇到需要將"樹形List"與"扁平List"互轉(zhuǎn)的情況,本文為大家整理了Java實現(xiàn)樹形List與扁平List互轉(zhuǎn)的示例代碼,希望對大家有所幫助2023-05-05如何用idea編寫并運行第一個spark scala處理程序
詳細介紹了如何使用IntelliJ IDEA創(chuàng)建Scala項目,包括配置JDK和Scala SDK,添加Maven支持,編輯pom.xml,并創(chuàng)建及運行Scala程序,這為Scala初學者提供了一個基礎的項目搭建和運行指南2024-09-09SpringBoot+Idea熱部署實現(xiàn)流程解析
這篇文章主要介紹了SpringBoot+Idea熱部署實現(xiàn)流程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-11-11Java中注解@JsonFormat與@DateTimeFormat的使用
從數(shù)據(jù)庫獲取時間傳到前端進行展示的時候,我們有時候可能無法得到一個滿意的時間格式的時間日期,本文主要介紹了Java中注解@JsonFormat與@DateTimeFormat的使用,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧2023-08-08