美文网首页
Springboot 关于缓存的一些思路 关于Mysql Red

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

作者: zzzmh | 来源:发表于2019-07-12 14:50 被阅读0次

    前言

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

    相关文章

      网友评论

          本文标题:Springboot 关于缓存的一些思路 关于Mysql Red

          本文链接:https://www.haomeiwen.com/subject/nrnekctx.html