Springboot 关于缓存的一些思路 关于Mysql Redis home 编辑时间 2019/06/11 ![](/api/file/getImage?fileId=5cff447f16199b0683001cbc) ## 前言 **关于 `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 我相信网上的方法比这个好的还有很多。但很多东西还是要自己去试着做一遍才了解其流程、规律。 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay Nginx 跨域配置 支持多域名 (解决配置无效问题) 关于Nginx日志输出格式问题