前言
關於 Springboot AOP
集成控制 Redis
實現緩存
網上有很多相關的例子,我也稍微瞭解了一二。
但我習慣還是自己折騰一遍,便於理解整個過程。
所以本文的方法和網上的略有不同,也可能不是最優解,只是記錄自己折騰的過程。
方法1 AOP + Redis
本方法適合一些訪問頻率較高,響應時間較長的Controller,具體就是查SQL拼JSON的過程會比較慢的Controller,對數據實時程度要求也不那麼高的話,第一次跑完把結果存redis,之後一段時間內直接讀redis來返回結果即可。
具體流程如下
- 通過AOP 非侵入性實現,不破壞原來的Controller
- 用方法名+參數JSON取MD5
- MD5作爲key,返回值JsonString作爲value存redis
- 執行前查詢redis存在緩存則直接返回緩存數據
- 不存在緩存正常執行方法,執行完成後保存緩存數據
親測速度可以從2000ms 提升到 20ms
主要代碼如下
/**
* AOP 切面 用於緩存數據
*/
@Aspect
@Component
public class ApiControllerCacheAspect {
private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
/**
* 默認過期時長 3小時
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 3;
@Autowired
private RedisUtils redisUtils;
// 切面的對象,這裏指定了Controller來防止不需要緩存的也被緩存
@Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
public void loginPointCut() {
}
@Around("loginPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
R result = R.error();
try {
// 從切面中獲取方法名 + 參數名
String methodName = ((MethodSignature) point.getSignature()).getMethod().getName();
String params = JSON.toJSONString(point.getArgs());
// 轉換成md5
String md5 = DigestUtils.md5Hex(methodName + params);
// 從redis獲取緩存
String cache = redisUtils.get(md5);
if (StringUtils.isBlank(cache)) {
// 讀不到緩存正常執行方法
result = (R) point.proceed();
// 執行完畢後結果寫入Redis緩存
redisUtils.set(md5, result.get("result"), DEFAULT_EXPIRE);
} else {
// 讀取到緩存直接返回,不執行方法
result = R.ok(JSON.parseArray(cache));
}
} catch (RRException e) {
result.put("code", e.getCode());
result.put("msg", e.getMsg());
} catch (Exception e) {
log.error("AOP 執行中異常 :" + e.toString());
e.printStackTrace();
}
return result;
}
}
方法1.5 AOP + Redis 加強進階版
這幾天在折騰過程中發現,按照Controller來緩存,顆粒太粗,
一些Controller 或 Controller裏的一些方法不需要緩存。
另外返回碼正確的才需要緩存,返回錯誤不應執行緩存。
於事想到用自定義註解搭配AOP來實現精細化的緩存
由於涉及的代碼的地方較多,就選最主要的貼出來講了
具體流程如下
- 先實現一個自定義註解 Cache.java 參數time 默認0
- 在需要緩存的method上加註解@Cache
- 若參數time = 0 說明沒有設置緩存時間,根據統一配置時間緩存
- 若參數time != 0 說明設置過緩存時間,按照設置的時間緩存
- 如果沒有緩存正常執行方法,結束執行後先驗證狀態碼,正確的才緩存本次數據。
註解類 /annotation/Cache.java
/**
* 緩存控制
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
/**
* 緩存時間
* 有該註解的全部緩存
* time默認0 爲根據數據庫sys_config配置時間爲準
* time非0 根據註解時間緩存
* */
long time() default 0;
}
需要緩存的Controller
// 指定緩存時間
@Cache(time = 3600L)
@PostMapping("getDataList")
public R getDataList() {
return R.ok();
}
// 不指定緩存時間 通過統一配置的時間緩存
@Cache
@PostMapping("getDataList")
public R getDataList() {
return R.ok();
}
AOP切面的核心代碼
/**
* AOP 切面 用於緩存數據
*/
@Aspect
@Component
public class ApiControllerCacheAspect {
private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
/**
* 默認過期時長 3小時
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 3;
@Autowired
private RedisUtils redisUtils;
// 切面的對象,這裏指定了Controller來防止不需要緩存的也被緩存
@Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
public void loginPointCut() {
}
@Around("loginPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
R result = R.error();
try {
// 從切面中獲取方法名 + 參數名
Method method = ((MethodSignature) point.getSignature()).getMethod();
String methodName = method.getName();
// 不支持參數包含HttpServletRequest等 如需要建議用@Autowired注入
String params = point.getArgs() == null ? "" : JSON.toJSONString(point.getArgs());
Cache cache = method.getAnnotation(Cache.class);
// 不爲空說明該方法有此註解
if (cache != null) {
// 從redis獲取緩存
String jsonString = redisUtils.get(md5);
if (StringUtils.isBlank(jsonString)) {
// 讀不到緩存正常執行方法
result = (R) point.proceed();
// 執行完畢後結果寫入Redis緩存 只緩存正確數據
if((int) result.get("code") == 0){
// Cache.time() 默認值是0 如等於0 使用統一緩存時間 如不等於0 說明需要自定義,用Cache.time()
long time = cache.time() != 0 ? cache.time() : DEFAULT_EXPIRE;
redisUtils.set(md5, result.get("result"), time);
}
} else {
// 讀取到緩存直接返回,不執行方法
result = R.ok(JSON.parseArray(jsonString));
}
}else{
result = (R) point.proceed();
}
} catch (RRException e) {
result.put("code", e.getCode());
result.put("msg", e.getMsg());
} catch (Exception e) {
log.error("AOP 執行中異常 :" + e.toString());
e.printStackTrace();
}
return result;
}
}
再補充一種需求
如果特殊情況下前端不希望某次請求讀取到緩存,在 request -> header
中加入 no-cache
來阻止緩存。
// 在切面中加入獲取 request
HttpServletRequest request = (HttpServletRequest) RequestContextHolder.getRequestAttributes().resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 獲取 header
String NoCache = request.getHeader("no-cache");
// 判斷是否緩存中加入NoCache字段判斷
/* 例如:
* if("true".equalsIgnoreCase(NoCache))
* 不走緩存 直接正常查詢SQL返回
*
* 除此之外如果有需要嚴格禁止緩存的話?
* Mysql的查詢語句也可以加上 SQL_NO_CACHE 來防止Mysql緩存
*/
方法2 Redis + Mysql
核心思路
拋棄Mysql,以Redis數據爲主讀寫,Mysql作爲備份方案
直接在Redis進行數據讀寫,
Mysql開一張表也是Key Value記錄數據,
最大長度支持到varchar(20000)
每次Redis寫數據完成後,都再異步處理整個Value存一次Mysql。
每次Redis讀取數據都判斷一下是否讀到,
讀不到的時候再去Mysql讀,
Mysql讀到就存redis並返回。
好處就是最大化讀寫速度,
缺點是最大長度不能超過2萬、
特殊情況下也會造成數據丟失等。
只能說是一定程度下減少Redis數據丟失風險,
只需要備份Mysql即可。
未完待續
END
我相信網上的方法比這個好的還有很多。但很多東西還是要自己去試着做一遍才瞭解其流程、規律。
本文同時也會發布在我的個人博客
https://zzzmh.cn/single?id=68