SpringBoot緩存方法返回值的方法詳解
Why
為什么要對(duì)方法的返回值進(jìn)行緩存呢?
簡(jiǎn)單來(lái)說(shuō)是為了提升后端程序的性能和提高前端程序的訪問速度。減小對(duì)db和后端應(yīng)用程序的壓力。
一般而言,緩存的內(nèi)容都是不經(jīng)常變化的,或者輕微變化對(duì)于前端應(yīng)用程序是可以容忍的。
否則,不建議加入緩存,因?yàn)樵黾泳彺鏁?huì)使程序復(fù)雜度增加,還會(huì)出現(xiàn)一些其他的問題,比如緩存同步,數(shù)據(jù)一致性,更甚者,可能出現(xiàn)經(jīng)典的緩存穿透、緩存擊穿、緩存雪崩問題。
HowDo
如何緩存方法的返回值?應(yīng)該會(huì)有很多的辦法,本文簡(jiǎn)單描述兩個(gè)比較常見并且比較容易實(shí)現(xiàn)的辦法:
- 自定義注解
- SpringCache
annotation
整體思路:
第一步:定義一個(gè)自定義注解,在需要緩存的方法上面添加此注解,當(dāng)調(diào)用該方法的時(shí)候,方法返回值將被緩存起來(lái),下次再調(diào)用的時(shí)候?qū)⒉粫?huì)進(jìn)入該方法。其中需要指定一個(gè)緩存鍵用來(lái)區(qū)分不同的調(diào)用,建議為:類名+方法名+參數(shù)名
第二步:編寫該注解的切面,根據(jù)緩存鍵查詢緩存池,若池中已經(jīng)存在則直接返回不執(zhí)行方法;若不存在,將執(zhí)行方法,并在方法執(zhí)行完畢寫入緩沖池中。方法如果拋異常了,將不會(huì)創(chuàng)建緩存
第三步:緩存池,首先需要盡量保證緩存池是線程安全的,當(dāng)然了沒有絕對(duì)的線程安全。其次為了不發(fā)生緩存臃腫的問題,可以提供緩存釋放的能力。另外,緩存池應(yīng)該設(shè)計(jì)為可替代,比如可以絲滑得在使用程序內(nèi)存和使用redis直接調(diào)整。
MethodCache
創(chuàng)建一個(gè)名為MethodCache 的自定義注解
package com.ramble.methodcache.annotation; import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface MethodCache { }
MethodCacheAspect
編寫MethodCache注解的切面實(shí)現(xiàn)
package com.ramble.methodcache.annotation; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Aspect @Component public class MethodCacheAspect { private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<>(); @Around(value = "@annotation(methodCache)") public Object around(ProceedingJoinPoint jp, MethodCache methodCache) throws Throwable { String className = jp.getSignature().getDeclaringType().getSimpleName(); String methodName = jp.getSignature().getName(); String args = String.join(",", Arrays.toString(jp.getArgs())); String key = className + ":" + methodName + ":" + args; // key 示例:DemoController:findUser:[FindUserParam(id=1, name=c7)] log.debug("緩存的key={}", key); Object cache = getCache(key); if (null != cache) { log.debug("走緩存"); return cache; } else { log.debug("不走緩存"); Object value = jp.proceed(); setCache(key, value); return value; } } private Object getCache(String key) { return CACHE_MAP.get(key); } private void setCache(String key, Object value) { CACHE_MAP.put(key, value); } }
- Around:對(duì)被MethodCache注解修飾的方法啟用環(huán)繞通知
- ProceedingJoinPoint:通過(guò)此對(duì)象獲取方法所在類、方法名和參數(shù),用來(lái)組裝緩存key
- CACHE_MAP:緩存池,生產(chǎn)環(huán)境建議使用redis等可以分布式存儲(chǔ)的容器,直接放程序內(nèi)存不利于后期業(yè)務(wù)擴(kuò)張后多實(shí)例部署
controller
package com.ramble.methodcache.controller; import com.ramble.methodcache.annotation.MethodCache; import com.ramble.methodcache.controller.param.CreateUserParam; import com.ramble.methodcache.controller.param.FindUserParam; import com.ramble.methodcache.service.DemoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @Tag(name = "demo - api") @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/demo") public class DemoController { private final DemoService demoService; @MethodCache @GetMapping("/{id}") public String getUser(@PathVariable("id") String id) { return demoService.getUser(id); } @Operation(summary = "查詢用戶") @MethodCache @PostMapping("/list") public String findUser(@RequestBody FindUserParam param) { return demoService.findUser(param); } }
通過(guò)反復(fù)調(diào)用被@MethodCache注解修飾的方法,會(huì)發(fā)現(xiàn)若緩存池有數(shù)據(jù),將不會(huì)進(jìn)入方法體。
SpringCache
其實(shí)SpringCache的實(shí)現(xiàn)思路和上述方法基本一致,SpringCache提供了更優(yōu)雅的編程方式,更絲滑的緩存池切換和管理,更強(qiáng)大的功能和統(tǒng)一規(guī)范。
EnableCaching
使用 @EnableCaching 開啟SpringCache功能,無(wú)需引入額外的pom。
默認(rèn)情況下,緩存池將由 ConcurrentMapCacheManager 這個(gè)對(duì)象管理,也就是默認(rèn)是程序內(nèi)存中緩存。其中用于存放緩存數(shù)據(jù)的是一個(gè) ConcurrentHashMap,源碼如下:
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware { private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16); ...... }
此外可選的緩存池管理對(duì)象還有:
- EhCacheCacheManager
- JCacheCacheManager
- RedisCacheManager
- ......
Cacheable
package com.ramble.methodcache.controller; import com.ramble.methodcache.controller.param.FindUserParam; import com.ramble.methodcache.service.DemoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.web.bind.annotation.*; @Tag(name = "user - api") @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/user") public class UserController { private final DemoService demoService; @Cacheable(value = "userCache") @GetMapping("/{id}") public String getUser(@PathVariable("id") String id) { return demoService.getUser(id); } @Operation(summary = "查詢用戶") @Cacheable(value = "userCache") @PostMapping("/list") public String findUser(@RequestBody FindUserParam param) { return demoService.findUser(param); } }
- 使用@Cacheable注解修飾需要緩存返回值的方法
- value必填,不然運(yùn)行時(shí)報(bào)異常。類似一個(gè)分組,將不同的數(shù)據(jù)或者方法(當(dāng)然也可以其他維度,主要看業(yè)務(wù)需要)放到一堆,便于管理
- 可以修飾接口方法,但是不建議,IDEA會(huì)報(bào)一個(gè)提示Spring doesn't recommend to annotate interface methods with @Cache* annotation
常用屬性:
- value:緩存名稱
- cacheNames:緩存名稱。value 和cacheNames都被AliasFor注解修飾,他們互為別名
- key:緩存數(shù)據(jù)時(shí)候的key,默認(rèn)使用方法參數(shù)的值,可以使用SpEL生產(chǎn)key
- keyGenerator:key生產(chǎn)器。和key二選一
- cacheManager:緩存管理器
- cacheResolver:和caheManager二選一,互為別名
- condition:創(chuàng)建緩存的條件,可用SpEL表達(dá)式(如#id>0,表示當(dāng)入?yún)d大于0時(shí)候才緩存方法返回值)
- unless:不創(chuàng)建緩存的條件,如#result==null,表示方法返回值為null的時(shí)候不緩存
CachePut
用來(lái)更新緩存。被CachePut注解修飾的方法,在被調(diào)用的時(shí)候不會(huì)校驗(yàn)緩存池中是否已經(jīng)存在緩存,會(huì)直接發(fā)起調(diào)用,然后將返回值放入緩存池中。
CacheEvict
用來(lái)刪除緩存,會(huì)根據(jù)key來(lái)刪除緩存中的數(shù)據(jù)。并且不會(huì)將本方法返回值緩存起來(lái)。
常用屬性:
- value/cacheeName:緩存名稱,或者說(shuō)緩存分組
- key:緩存數(shù)據(jù)的鍵
- allEntries:是否根據(jù)緩存名稱清空所有緩存,默認(rèn)為false。當(dāng)此值為true的時(shí)候,將根據(jù)cacheName清空緩存池中的數(shù)據(jù),然后將新的返回值放入緩存
- beforeInvocation:是否在方法執(zhí)行之前就清空緩存,默認(rèn)為false
Caching
此注解用于在一個(gè)方法或者類上面,同時(shí)指定多個(gè)SpringCache相關(guān)注解。這個(gè)也是SpringCache的強(qiáng)大之處,可以自定義各種緩存創(chuàng)建、更新、刪除的邏輯,應(yīng)對(duì)復(fù)雜的業(yè)務(wù)場(chǎng)景。
屬性:
- cacheable:指定@Cacheable注解
- put:指定@CachePut注解
- evict:指定@CacheEvict注解
源碼:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Caching { Cacheable[] cacheable() default {}; CachePut[] put() default {}; CacheEvict[] evict() default {}; }
相當(dāng)于就是注解里面套注解,用來(lái)完成復(fù)雜和多變的場(chǎng)景,這個(gè)設(shè)計(jì)相當(dāng)?shù)耐廴?/p>
CacheConfig
放在類上面,那么類中所有方法都會(huì)被緩存
SpringCacheEnv
SpringCache內(nèi)置了一些環(huán)境變量,可用于各個(gè)注解的屬性中。
- methodName:被修飾方法的方法名
- method:被修飾方法的Method對(duì)象
- target:被修飾方法所屬的類對(duì)象的實(shí)例
- targetClass:被修飾方法所屬類對(duì)象
- args:方法入?yún)?,是一個(gè) object[] 數(shù)組
- caches:這個(gè)對(duì)象其實(shí)就是ConcurrentMapCacheManager中的cacheMap,這個(gè)cacheMap呢就是一開頭提到的ConcurrentHashMap,即緩存池。caches的使用場(chǎng)景尚不明了。
- argumentName:方法的入?yún)?/li>
- result:方法執(zhí)行的返回值
使用示例:
@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null") public String showEnv() { return "打印各個(gè)環(huán)境變量"; }
表示僅當(dāng)方法返回值不為null的時(shí)候才緩存結(jié)果,這里通過(guò)result env 獲取返回值。
另外,condition 和 unless 為互補(bǔ)關(guān)系,上述condition = "#result!=null"和unless = "#result==null"其實(shí)是一個(gè)意思。
@Cacheable(value = "userCache", key = "#name") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用方法入?yún)⒆鳛樵摋l緩存數(shù)據(jù)的key,若傳入的name為gg,則實(shí)際緩存的數(shù)據(jù)為:gg->打印各個(gè)環(huán)境變量
另外,如果name為空會(huì)報(bào)異常,因?yàn)榫彺鎘ey不允許為null
@Cacheable(value = "userCache",key = "#root.args") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用方法的入?yún)⒆鳛榫彺娴膋ey,若傳遞的參數(shù)為id=100,name=gg,則實(shí)際緩存的數(shù)據(jù)為:Object[]->打印各個(gè)環(huán)境變量,Object[]數(shù)組中包含兩個(gè)值。
既然是數(shù)組,可以通過(guò)下標(biāo)進(jìn)行訪問,root.args[1] 表示獲取第二個(gè)參數(shù),本例中即 取 name 的值 gg,則實(shí)際緩存的數(shù)據(jù)為:gg->打印各個(gè)環(huán)境變量。
@Cacheable(value = "userCache",key = "#root.targetClass") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用被修飾的方法所屬的類作為緩存key,實(shí)際緩存的數(shù)據(jù)為:Class->打印各個(gè)環(huán)境變量,key為class對(duì)象,不是全限定名,全限定名是一個(gè)字符串,這里是class對(duì)象。
可是,不是很懂這樣設(shè)計(jì)的應(yīng)用場(chǎng)景是什么......
@Cacheable(value = "userCache",key = "#root.target") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用被修飾方法所屬類的實(shí)例作為key,實(shí)際緩存的數(shù)據(jù)為:UserController->打印各個(gè)環(huán)境變量。
被修飾的方法就是在UserController中,調(diào)試的時(shí)候甚至可以獲取到此實(shí)例注入的其它容器對(duì)象,如userService等。
可是,不是很懂這樣設(shè)計(jì)的應(yīng)用場(chǎng)景是什么......
@Cacheable(value = "userCache",key = "#root.method") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用Method對(duì)象作為緩存的key,是Method對(duì)象,不是字符串。
可是,不是很懂這樣設(shè)計(jì)的應(yīng)用場(chǎng)景是什么......
@Cacheable(value = "userCache",key = "#root.methodName") public String showEnv(String id, String name) { return "打印各個(gè)環(huán)境變量"; }
表示使用方法名作為緩存的key,就是一個(gè)字符串。
如何獲取緩存的數(shù)據(jù)?
ConcurrentMapCacheManager的cacheMap是一個(gè)私有變量,所以沒有辦法可以打印緩存池中的數(shù)據(jù),不過(guò)可以通過(guò)調(diào)試的方式進(jìn)入對(duì)象內(nèi)部查看。如下:
@Tag(name = "user - api") @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/user") public class UserController { private final ConcurrentMapCacheManager cacheManager; /** * 只有調(diào)試才課可以查看緩存池中的數(shù)據(jù) */ @GetMapping("/cache") public void showCacheData() { //需要debug進(jìn)入 Collection<String> cacheNames = cacheManager.getCacheNames(); } }
總結(jié)
雖然提供了很多的環(huán)境變量,但是大多都無(wú)法找到對(duì)應(yīng)的使用場(chǎng)景,其實(shí)在實(shí)際開發(fā)中,最常見的就是key的生產(chǎn),一般而言使用類名+方法名+參數(shù)值足矣。
以上就是SpringBoot緩存方法返回值的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot緩存方法返回值的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Maven dependencies與dependencyManagement的區(qū)別詳解
這篇文章主要介紹了Maven dependencies與dependencyManagement的區(qū)別詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04spring boot 使用@Async實(shí)現(xiàn)異步調(diào)用方法
本篇文章主要介紹了spring boot 使用@Async實(shí)現(xiàn)異步調(diào)用方法,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-04-04Spring?Cloud?Stream消息驅(qū)動(dòng)組件使用方法介紹
Spring?Cloud?Stream?消息驅(qū)動(dòng)組件幫助我們更快速,更方便,更友好的去構(gòu)建消息驅(qū)動(dòng)微服務(wù)的。當(dāng)時(shí)定時(shí)任務(wù)和消息驅(qū)動(dòng)的?個(gè)對(duì)比。消息驅(qū)動(dòng):基于消息機(jī)制做一些事情2022-09-09Java8默認(rèn)方法Default Methods原理及實(shí)例詳解
這篇文章主要介紹了Java8默認(rèn)方法Default Methods原理及實(shí)例詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01java實(shí)現(xiàn)簡(jiǎn)單注冊(cè)選擇所在城市
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單注冊(cè)選擇所在城市的相關(guān)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-04-04Mybatis實(shí)體類對(duì)象入?yún)⒉樵兊墓P記
這篇文章主要介紹了Mybatis實(shí)體類對(duì)象入?yún)⒉樵兊墓P記,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06使用socket進(jìn)行服務(wù)端與客戶端傳文件的方法
這篇文章主要介紹了使用socket進(jìn)行服務(wù)端與客戶端傳文件的方法,需要的朋友可以參考下2017-08-08