Springboot 关于缓存的一些思路 关于Mysql Redis

前言

关于 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章