前言

关于 Springboot AOP 集成控制 Redis实现缓存
网上有很多相关的例子,我也稍微了解了一二。
但我习惯还是自己折腾一遍,便于理解整个过程。
所以本文的方法和网上的略有不同,也可能不是最优解,只是记录自己折腾的过程。

方法1 AOP + Redis

本方法适合一些访问频率较高,响应时间较长的Controller,具体就是查SQL拼JSON的过程会比较慢的Controller,对数据实时程度要求也不那么高的话,第一次跑完把结果存redis,之后一段时间内直接读redis来返回结果即可。

具体流程如下
* 通过AOP 非侵入性实现,不破坏原来的Controller
* 用方法名+参数JSON取MD5
* MD5作为key,返回值JsonString作为value存redis
* 执行前查询redis存在缓存则直接返回缓存数据
* 不存在缓存正常执行方法,执行完成后保存缓存数据

亲测速度可以从2000ms 提升到 20ms

主要代码如下

  1. /**
  2. * AOP 切面 用于缓存数据
  3. */
  4. @Aspect
  5. @Component
  6. public class ApiControllerCacheAspect {
  7. private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
  8. /**
  9. * 默认过期时长 3小时
  10. */
  11. public final static long DEFAULT_EXPIRE = 60 * 60 * 3;
  12. @Autowired
  13. private RedisUtils redisUtils;
  14. // 切面的对象,这里指定了Controller来防止不需要缓存的也被缓存
  15. @Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
  16. public void loginPointCut() {
  17. }
  18. @Around("loginPointCut()")
  19. public Object around(ProceedingJoinPoint point) throws Throwable {
  20. R result = R.error();
  21. try {
  22. // 从切面中获取方法名 + 参数名
  23. String methodName = ((MethodSignature) point.getSignature()).getMethod().getName();
  24. String params = JSON.toJSONString(point.getArgs());
  25. // 转换成md5
  26. String md5 = DigestUtils.md5Hex(methodName + params);
  27. // 从redis获取缓存
  28. String cache = redisUtils.get(md5);
  29. if (StringUtils.isBlank(cache)) {
  30. // 读不到缓存正常执行方法
  31. result = (R) point.proceed();
  32. // 执行完毕后结果写入Redis缓存
  33. redisUtils.set(md5, result.get("result"), DEFAULT_EXPIRE);
  34. } else {
  35. // 读取到缓存直接返回,不执行方法
  36. result = R.ok(JSON.parseArray(cache));
  37. }
  38. } catch (RRException e) {
  39. result.put("code", e.getCode());
  40. result.put("msg", e.getMsg());
  41. } catch (Exception e) {
  42. log.error("AOP 执行中异常 :" + e.toString());
  43. e.printStackTrace();
  44. }
  45. return result;
  46. }
  47. }

方法1.5 AOP + Redis 加强进阶版

这几天在折腾过程中发现,按照Controller来缓存,颗粒太粗,
一些Controller 或 Controller里的一些方法不需要缓存。
另外返回码正确的才需要缓存,返回错误不应执行缓存。
于事想到用自定义注解搭配AOP来实现精细化的缓存
由于涉及的代码的地方较多,就选最主要的贴出来讲了

具体流程如下
* 先实现一个自定义注解 Cache.java 参数time 默认0
* 在需要缓存的method上加注解@Cache
* 若参数time = 0 说明没有设置缓存时间,根据统一配置时间缓存
* 若参数time != 0 说明设置过缓存时间,按照设置的时间缓存
* 如果没有缓存正常执行方法,结束执行后先验证状态码,正确的才缓存本次数据。

注解类 /annotation/Cache.java

  1. /**
  2. * 缓存控制
  3. */
  4. @Target(ElementType.METHOD)
  5. @Retention(RetentionPolicy.RUNTIME)
  6. public @interface Cache {
  7. /**
  8. * 缓存时间
  9. * 有该注解的全部缓存
  10. * time默认0 为根据数据库sys_config配置时间为准
  11. * time非0 根据注解时间缓存
  12. * */
  13. long time() default 0;
  14. }

需要缓存的Controller

  1. // 指定缓存时间
  2. @Cache(time = 3600L)
  3. @PostMapping("getDataList")
  4. public R getDataList() {
  5. return R.ok();
  6. }
  7. // 不指定缓存时间 通过统一配置的时间缓存
  8. @Cache
  9. @PostMapping("getDataList")
  10. public R getDataList() {
  11. return R.ok();
  12. }

AOP切面的核心代码

  1. /**
  2. * AOP 切面 用于缓存数据
  3. */
  4. @Aspect
  5. @Component
  6. public class ApiControllerCacheAspect {
  7. private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
  8. /**
  9. * 默认过期时长 3小时
  10. */
  11. public final static long DEFAULT_EXPIRE = 60 * 60 * 3;
  12. @Autowired
  13. private RedisUtils redisUtils;
  14. // 切面的对象,这里指定了Controller来防止不需要缓存的也被缓存
  15. @Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
  16. public void loginPointCut() {
  17. }
  18. @Around("loginPointCut()")
  19. public Object around(ProceedingJoinPoint point) throws Throwable {
  20. R result = R.error();
  21. try {
  22. // 从切面中获取方法名 + 参数名
  23. Method method = ((MethodSignature) point.getSignature()).getMethod();
  24. String methodName = method.getName();
  25. // 不支持参数包含HttpServletRequest等 如需要建议用@Autowired注入
  26. String params = point.getArgs() == null ? "" : JSON.toJSONString(point.getArgs());
  27. Cache cache = method.getAnnotation(Cache.class);
  28. // 不为空说明该方法有此注解
  29. if (cache != null) {
  30. // 从redis获取缓存
  31. String jsonString = redisUtils.get(md5);
  32. if (StringUtils.isBlank(jsonString)) {
  33. // 读不到缓存正常执行方法
  34. result = (R) point.proceed();
  35. // 执行完毕后结果写入Redis缓存 只缓存正确数据
  36. if((int) result.get("code") == 0){
  37. // Cache.time() 默认值是0 如等于0 使用统一缓存时间 如不等于0 说明需要自定义,用Cache.time()
  38. long time = cache.time() != 0 ? cache.time() : DEFAULT_EXPIRE;
  39. redisUtils.set(md5, result.get("result"), time);
  40. }
  41. } else {
  42. // 读取到缓存直接返回,不执行方法
  43. result = R.ok(JSON.parseArray(jsonString));
  44. }
  45. }else{
  46. result = (R) point.proceed();
  47. }
  48. } catch (RRException e) {
  49. result.put("code", e.getCode());
  50. result.put("msg", e.getMsg());
  51. } catch (Exception e) {
  52. log.error("AOP 执行中异常 :" + e.toString());
  53. e.printStackTrace();
  54. }
  55. return result;
  56. }
  57. }

再补充一种需求
如果特殊情况下前端不希望某次请求读取到缓存,在 request -> header 中加入 no-cache 来阻止缓存。

  1. // 在切面中加入获取 request
  2. HttpServletRequest request = (HttpServletRequest) RequestContextHolder.getRequestAttributes().resolveReference(RequestAttributes.REFERENCE_REQUEST);
  3. // 获取 header
  4. String NoCache = request.getHeader("no-cache");
  5. // 判断是否缓存中加入NoCache字段判断
  6. /* 例如:
  7. * if("true".equalsIgnoreCase(NoCache))
  8. * 不走缓存 直接正常查询SQL返回
  9. *
  10. * 除此之外如果有需要严格禁止缓存的话?
  11. * Mysql的查询语句也可以加上 SQL_NO_CACHE 来防止Mysql缓存
  12. */

方法2 Redis + Mysql

核心思路
抛弃Mysql,以Redis数据为主读写,Mysql作为备份方案
直接在Redis进行数据读写,
Mysql开一张表也是Key Value记录数据,
最大长度支持到varchar(20000)
每次Redis写数据完成后,都再异步处理整个Value存一次Mysql。
每次Redis读取数据都判断一下是否读到,
读不到的时候再去Mysql读,
Mysql读到就存redis并返回。
好处就是最大化读写速度,
缺点是最大长度不能超过2万、
特殊情况下也会造成数据丢失等。
只能说是一定程度下减少Redis数据丢失风险,
只需要备份Mysql即可。

未完待续

END

我相信网上的方法比这个好的还有很多。但很多东西还是要自己去试着做一遍才了解其流程、规律。