為什麼要對方法的返回值進行快取呢?
簡單來說是為了提升後端程式的效能和提高前端程式的存取速度。減小對db和後端應用程式的壓力。
一般而言,快取的內容都是不經常變化的,或者輕微變化對於前端應用程式是可以容忍的。
否則,不建議加入快取,因為增加快取會使程式複雜度增加,還會出現一些其他的問題,比如快取同步,資料一致性,更甚者,可能出現經典的快取穿透、快取擊穿、快取雪崩問題。
如何快取方法的返回值?應該會有很多的辦法,本文簡單描述兩個比較常見並且比較容易實現的辦法:
整體思路:
第一步:定義一個自定義註解,在需要快取的方法上面新增此註解,當呼叫該方法的時候,方法返回值將被快取起來,下次再呼叫的時候將不會進入該方法。其中需要指定一個快取鍵用來區分不同的呼叫,建議為:類名+方法名+引數名
第二步:編寫該註解的切面,根據快取鍵查詢快取池,若池中已經存在則直接返回不執行方法;若不存在,將執行方法,並在方法執行完畢寫入緩衝池中。方法如果拋異常了,將不會建立快取
第三步:快取池,首先需要儘量保證快取池是執行緒安全的,當然了沒有絕對的執行緒安全。其次為了不發生快取臃腫的問題,可以提供快取釋放的能力。另外,快取池應該設計為可替代,比如可以絲滑得在使用程式記憶體和使用redis直接調整。
建立一個名為MethodCache 的自定義註解
package com.ramble.methodcache.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodCache {
}
編寫MethodCache註解的切面實現
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);
}
}
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);
}
}
通過反覆呼叫被@MethodCache註解修飾的方法,會發現若快取池有資料,將不會進入方法體。
其實SpringCache的實現思路和上述方法基本一致,SpringCache提供了更優雅的程式設計方式,更絲滑的快取池切換和管理,更強大的功能和統一規範。
使用 @EnableCaching 開啟SpringCache功能,無需引入額外的pom。
預設情況下,快取池將由 ConcurrentMapCacheManager 這個物件管理,也就是預設是程式記憶體中快取。其中用於存放快取資料的是一個 ConcurrentHashMap,原始碼如下:
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
......
}
此外可選的快取池管理物件還有:
EhCacheCacheManager
JCacheCacheManager
RedisCacheManager
......
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);
}
}
常用屬性:
用來更新快取。被CachePut註解修飾的方法,在被呼叫的時候不會校驗快取池中是否已經存在快取,會直接發起呼叫,然後將返回值放入快取池中。
用來刪除快取,會根據key來刪除快取中的資料。並且不會將本方法返回值快取起來。
常用屬性:
此註解用於在一個方法或者類上面,同時指定多個SpringCache相關注解。這個也是SpringCache的強大之處,可以自定義各種快取建立、更新、刪除的邏輯,應對複雜的業務場景。
屬性:
原始碼:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
相當於就是註解裡面套註解,用來完成複雜和多變的場景,這個設計相當的哇塞。
放在類上面,那麼類中所有方法都會被快取
SpringCache內建了一些環境變數,可用於各個註解的屬性中。
methodName:被修飾方法的方法名
method:被修飾方法的Method物件
target:被修飾方法所屬的類物件的範例
targetClass:被修飾方法所屬類物件
args:方法入參,是一個 object[] 陣列
caches:這個物件其實就是ConcurrentMapCacheManager中的cacheMap,這個cacheMap呢就是一開頭提到的ConcurrentHashMap,即快取池。caches的使用場景尚不明瞭。
argumentName:方法的入參
result:方法執行的返回值
使用範例:
@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null")
public String showEnv() {
return "列印各個環境變數";
}
表示僅當方法返回值不為null的時候才快取結果,這裡通過result env 獲取返回值。
另外,condition 和 unless 為互補關係,上述condition = "#result!=null"和unless = "#result==null"其實是一個意思。
@Cacheable(value = "userCache", key = "#name")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用方法入參作為該條快取資料的key,若傳入的name為gg,則實際快取的資料為:gg->列印各個環境變數
另外,如果name為空會報異常,因為快取key不允許為null
@Cacheable(value = "userCache",key = "#root.args")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用方法的入參作為快取的key,若傳遞的引數為id=100,name=gg,則實際快取的資料為:Object[]->列印各個環境變數,Object[]陣列中包含兩個值。
既然是陣列,可以通過下標進行存取,root.args[1] 表示獲取第二個引數,本例中即 取 name 的值 gg,則實際快取的資料為:gg->列印各個環境變數。
@Cacheable(value = "userCache",key = "#root.targetClass")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用被修飾的方法所屬的類作為快取key,實際快取的資料為:Class->列印各個環境變數,key為class物件,不是全限定名,全限定名是一個字串,這裡是class物件。
可是,不是很懂這樣設計的應用場景是什麼......
@Cacheable(value = "userCache",key = "#root.target")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用被修飾方法所屬類的範例作為key,實際快取的資料為:UserController->列印各個環境變數。
被修飾的方法就是在UserController中,偵錯的時候甚至可以獲取到此範例注入的其它容器物件,如userService等。
可是,不是很懂這樣設計的應用場景是什麼......
@Cacheable(value = "userCache",key = "#root.method")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用Method物件作為快取的key,是Method物件,不是字串。
可是,不是很懂這樣設計的應用場景是什麼......
@Cacheable(value = "userCache",key = "#root.methodName")
public String showEnv(String id, String name) {
return "列印各個環境變數";
}
表示使用方法名作為快取的key,就是一個字串。
如何獲取快取的資料?
ConcurrentMapCacheManager的cacheMap是一個私有變數,所以沒有辦法可以列印快取池中的資料,不過可以通過偵錯的方式進入物件內部檢視。如下:
@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {
private final ConcurrentMapCacheManager cacheManager;
/**
* 只有偵錯才課可以檢視快取池中的資料
*/
@GetMapping("/cache")
public void showCacheData() {
//需要debug進入
Collection<String> cacheNames = cacheManager.getCacheNames();
}
}
總結:
雖然提供了很多的環境變數,但是大多都無法找到對應的使用場景,其實在實際開發中,最常見的就是key的生產,一般而言使用類名+方法名+引數值足矣。
參考:https://juejin.cn/post/6987993458807930893