Springboot 实现Controller缓存的思路 关于Mysql Redis
2019-06-28
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
**关于 `Springboot AOP` 集成控制 `Redis`实现缓存**
网上有很多相关的例子,我也稍微了解了一二。
但我习惯还是自己折腾一遍,便于理解整个过程。
所以本文的方法和网上的略有不同,也可能不是最优解,只是记录自己折腾的过程。
## 方法1 AOP + Redis
本方法适合一些访问频率较高,响应时间较长的Controller,具体就是查SQL拼JSON的过程会比较慢的Controller,对数据实时程度要求也不那么高的话,第一次跑完把结果存redis,之后一段时间内直接读redis来返回结果即可。
**具体流程如下**
* 通过AOP 非侵入性实现,不破坏原来的Controller
* 用方法名+参数JSON取MD5
* MD5作为key,返回值JsonString作为value存redis
* 执行前查询redis存在缓存则直接返回缓存数据
* 不存在缓存正常执行方法,执行完成后保存缓存数据
亲测速度可以从2000ms 提升到 20ms
主要代码如下
```java
/**
* 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**
```java
/**
* 缓存控制
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
/**
* 缓存时间
* 有该注解的全部缓存
* time默认0 为根据数据库sys_config配置时间为准
* time非0 根据注解时间缓存
* */
long time() default 0;
}
```
**需要缓存的Controller**
```java
// 指定缓存时间
@Cache(time = 3600L)
@PostMapping("getDataList")
public R getDataList() {
return R.ok();
}
// 不指定缓存时间 通过统一配置的时间缓存
@Cache
@PostMapping("getDataList")
public R getDataList() {
return R.ok();
}
```
**AOP切面的核心代码**
```java
/**
* 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` 来阻止缓存。
```java
// 在切面中加入获取 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
我相信网上的方法比这个好的还有很多。但很多东西还是要自己去试着做一遍才了解其流程、规律。