暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis做缓存,怎么样才是正确的使用方法?

乐吧了吧 2021-06-21
1270

如何使用Redis做缓存


我们都知道Redis作为NoSql数据库的代表之一,通常会用来作为缓存使用。也是我在工作中通常使用的缓存之一。


1、我们什么时候缓存需要用到Redis?


我认为,缓存可以分为两大类:本地缓存和分布式缓存。当我们一个分布式系统就会考虑到缓存一致性的问题,所以需要使用到一个快速的、高并发的、灵活的存储服务,那么Redis就能很好的满足这些。


  • 本地缓存:
    即把缓存信息存储到应用内存内部,不能跨应用读取。所以这样的缓存的读写效率上是非常高的,因为节省了http的调用时间。问题是不能跨服务读取,在分布式系统中可能会找成每个机器缓存内容不同的问题。

  • 分布式缓存:
    即把缓存内容存储到单独的缓存系统中,当调用时,去指定缓存服务取数据,因此就不会出现本地缓存的多系统缓存数据不同的问题。


SpringBoot连接Redis配置(本来懒得写的, 但是我还是追求完美一点):


复制
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.2</version>
    </dependency>
    复制

    复制


    RedisClient我使用的是SpringBoot2.0后自带的lettuce框架,而并非jredis。


    复制
      spring.redis.database=0
      # Redis服务器地址
      spring.redis.host=81.70.xx.xx记得改喽,如果没有,可以私信我,我吧我的告诉你
      # Redis服务器连接端口
      spring.redis.port=6379
      spring.redis.timeout=5000
      # Redis服务器连接密码(默认为空)
      spring.redis.password=zxxx
      spring.redis.lettuce.pool.max-active=8
      spring.redis.lettuce.pool.min-idle=1
      spring.redis.lettuce.pool.max-idle=8
      spring.redis.lettuce.pool.max-wait=500ms
      spring.redis.lettuce.shutdown-timeout=100ms
      复制

      复制


      2、 缓存雏形 - 根据业务逻辑手撸代码


      在不需要大面积使用缓存的系统中,我们通常把Redis作为一种中间工具去使用。需在代码逻辑中加入自己的判断。


      复制
            public String baseCache(String name) {
        if(StringUtils.isBlank(name)){
        logger.error("Into BaseCache Service, Name is null.");
        return null;
        }
        logger.info("Into BaseCache Service, {}", name);
        //手动加入缓存逻辑
        String value = stringRedisTemplate.opsForValue().get("cache_sign:" + name);
        if(!StringUtils.isBlank(value)){
        return value;
        }
        else{
        value = String.valueOf(++BASE_CACHE_SIGN);
        stringRedisTemplate.opsForValue().set("cache_sign:" + name, value, 60, TimeUnit.SECONDS);
        return String.valueOf(BASE_CACHE_SIGN);
        }
        }
        复制

        复制


        3、通用缓存 - 使用Aop或者Interceptor实现


        个别接口或方法我们可以手撸代码,但是不管是后期维护还是代码的通用性都是比较局限的。所以与其在业务逻辑中增加判断逻辑,不如写一个通用的。


        3.1 先定义一个注解


        我们通过这个注解来区别方法是否需要缓存,注解放到方法上,此方法的返回结果将会被缓存。


        复制
          @Retention(RetentionPolicy.RUNTIME)
          @Target(ElementType.METHOD)
          public @interface UseCache {}
          复制

          复制


          3.2 使用SpringMvc的拦截器,对接口结果进行缓存。


          我们将从Redis取缓存结果提取到拦截器中,这样我们就可以只通过一个注解去标识是否执行缓存操作。


          3.2.1 拦截器: HandleInterceptorAdapter


          拦截器的作用我在这里就不过多的说明。如果在拦截器中发现此接口包含UseCache注解,我们需要检查Redis是否存在缓存,如果存在缓存,则直接返回其值即可。


          代码如下:


          复制
            /**
            * 缓存拦截器
            */
            @Component
            public class CustomCacheInterceptor extends HandlerInterceptorAdapter {
            private static final Logger logger = LoggerFactory.getLogger(CustomCacheInterceptor.class);
            /** RedisClient */
            private final StringRedisTemplate stringRedisTemplate;
            @Autowired
            public CustomCacheInterceptor(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
            }
            /**
            * 我们只需要实现preHandle方法即可,此方法会在接口调用前被调用,所以可以在这里判断缓存,如果存在缓存,直接返回即可。
            */
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            logger.info("Into Controller Before. This handler :{}", handler.getClass().getName());
            if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            //判断是否存在我们定义的缓存注解
            boolean useCache = method.hasMethodAnnotation(UseCache.class);
            //我们只对json进行缓存,其他的同理,所以再判断一下这个Controller是哪种接口方式。(包名+方法名+参数)
            boolean methodResponseBody = method.hasMethodAnnotation(ResponseBody.class);
            boolean classResponseBody = method.getBeanType().isAnnotationPresent(ResponseBody.class);
            boolean restController = method.getBeanType().isAnnotationPresent(RestController.class);

            if (useCache && (methodResponseBody || classResponseBody || restController)) {
            logger.info("This Method:{} Is UseCache", method.getMethod().getName());
            //我们使用一个工具类去生成这个方法的一个唯一key,使用此key当作redisKey。
            String cacheKey = CacheUtils.keySerialization(request, method.getMethod());
            //从Redis中取数据
            String responseValue = stringRedisTemplate.opsForValue().get(cacheKey);
            if (StringUtils.isNoneBlank(responseValue)) {
            //此方法存在缓存,且拿到了缓存值,所以直接返回给客户端即可,不需要再继续下一步
            PrintWriter writer = response.getWriter();
            writer.append(responseValue);
            writer.flush();
            writer.close();
            response.flushBuffer();
            return false;
            }
            }
            }
            return true;
            }
            }
            复制

            复制


            3.2.2 ResponseBodyAdvice 和 @ControllerAdvice


            上述中我们在拦截器中拦截了使用缓存且存在缓存的请求,直接返回缓存内容。但是还存在一个问题:我们从哪个地方将数据写入Redis?


            我之前考虑再重写HandleInterceptorAdapter.postHandle(...)方法,然后在处理完成Controller后,拦截处理结果,将结果放入Redis。但是出现以下问题:

            • 虽然能够正常调用postHandle(...)方法,但是大多进行缓存的都是ResponseBody数据,这样的数据并不会存放到ModleAndView中,当然也不会在DispatcherServlet中处理ModleAndView。所以并不能从ModleAndView中获取执行结果。

            • 我打算从response中找到要返回到客户端的数据。但是从上述方法我们就可以知道,response发送数据是使用流的方式,当Controller执行结束之后,postHandle之前就把数据写入了流中。如果重置输出流太过麻烦。


            所以我不能继续使用此拦截器去获取结果。


            解决:在调用完Controller之后,response写出之前,Springboot会调用一个通知:org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice。所以我们就可以实现这个接口去对response的body数据进行处理。


            代码如下:

            复制
              /**
              * SpringBoot提供RestControllerAdvice注解,此注解为使用@ResponseBody的Controller生成一个Aop通知。
              * 然后我们实现了ResponseBodyAdvice的方法:supports(...) 和 beforeBodyWrite(...)
              */
              @RestControllerAdvice
              public class ControllerResponseBody implements ResponseBodyAdvice<Object> {
              private static final Logger logger = LoggerFactory.getLogger(ControllerResponseBody.class);
              /** RedisClient */
              private final StringRedisTemplate redisTemplate;
              @Autowired
              public ApiResponseBody(StringRedisTemplate redisTemplate) {
              this.redisTemplate = redisTemplate;
              }


              /**
              * 此方法返回boolean类型值,主要是通过返回值确认是否走beforeBodyWrite方法
              */
              @Override
              public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
              logger.info("Into supports");
              Method method = returnType.getMethod();
              if(method != null){
              logger.info("Find This method use cache.");
              return method.isAnnotationPresent(UseCache.class);
              }
              return false;
              }


              /**
              * 这个方法调用在response响应之前,且方法参数是包含Controller的处理结果的。
              */
              @Override
              public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
              Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
              try{
              //将返回值转换为json,方便存储到redis。
              String value = JsonUtils.toGJsonString(body);
              // 拼接key
              String cacheKey = CacheUtils.keySerialization(request, returnType.getMethod());
              if(StringUtils.isNoneBlank(cacheKey)){
              // 设置缓存60s
              redisTemplate.opsForValue().set(cacheKey, value, 60, TimeUnit.SECONDS);
              }
              logger.info("cache controller return content.");
              }catch (Exception e){
              logger.error("Cache Exception:{}", e.getMessage(), e);
              }
              return body;
              }
              }
              复制

              复制


              3.2.3 使用测试


              1. 我们设置一下redis,使用SpringBoot默认的Lettuce,因为设置比较简便,而且呢,据说性能也不错,毕竟能让Springboot默认至此,不会差到哪里去


              复制
                # Redis数据库索引(默认为0)
                spring.redis.database=0
                # Redis服务器地址
                spring.redis.host=172.0.0.1
                # Redis服务器连接端口
                spring.redis.port=6379
                spring.redis.timeout=500
                # Redis服务器连接密码(默认为空)
                spring.redis.password=xxx
                spring.redis.lettuce.pool.max-active=8
                spring.redis.lettuce.pool.min-idle=1
                spring.redis.lettuce.pool.max-idle=8
                spring.redis.lettuce.pool.max-wait=500ms
                spring.redis.lettuce.shutdown-timeout=100ms
                复制

                复制


                1. 上面我们家了个拦截器,在Spring中我们通过配置web.xml去注册拦截器,在SpringBoot中更加简单


                复制
                  @Configuration
                  public class WebConfig implements WebMvcConfigurer {
                  @Autowired
                  private CustomCacheInterceptor cacheInterceptor;


                  @Override
                  public void addInterceptors(InterceptorRegistry registry) {
                  registry.addInterceptor(cacheInterceptor).addPathPatterns("/cache/**");
                  }
                  }
                  复制

                  复制


                  1. 对缓存的操作我们分别在通知和拦截其中都已经实现,那么我们就可以使用了,在我们的接口方法中使用@UseCache注解。


                  复制
                    @RestController
                    @RequestMapping("/cache")
                    public class CacheController {
                    private static final Logger logger = LoggerFactory.getLogger(CacheController.class);
                    @Resource
                    private CacheService cacheService;


                    /**
                    * 使用@UseCache注解
                    */
                    @UseCache
                    @RequestMapping("interceptorCache")
                    private String interceptorCache(String name){
                    logger.info("Into BaseCache Controller, {}", name);
                    String result = cacheService.incr(name);
                    return "OK" + " - " + name + " - " + result;
                    }
                    }
                    复制

                    复制


                    我就不再复制结果了,自己试一试吧
                    复制


                    3.2.4 反馈


                    这个是一个最基础的缓存了,可以通过自己需求去扩展:如使用spel为注解UseCache自定义缓存key、自定义缓存时间等等。

                    • 我们使用拦截器的方法有一个局限,即只能对请求的整个接口去做缓存,但是有些时候我们的需求不是对整个接口进行缓存,可能只想对service缓存,可能想对某个sql缓存。所以局限性还是存在的。


                    3.3 使用Spring Aop + 注解实现缓存


                    上面我们说到了使用拦截器实现时,只能对整个接口进行缓存。所以我们换一种思路:面向切面编程,即使用AOP。

                    SpringBoot Aop专用包:


                    复制
                              <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-aop</artifactId>
                      </dependency>
                      复制

                      复制


                      3.3.1 我们对上一个的缓存再优化一下吧


                      通常我们使用缓存会存在各种不同的需求,如缓存key,缓存时间,缓存条件等等。所以我们学着CacheAble注解,使用Spel表达式自定义key和超时时间。

                      复制
                        /**
                        * 自定义注解使用缓存
                        */
                        @Retention(RetentionPolicy.RUNTIME)
                        @Target({ElementType.METHOD})
                        @Documented
                        public @interface UseAopCache {
                        /**
                        * 自定义缓存Key,支持spel表达式
                        */
                        String customKey() default "" ;
                        /**
                        * 超时时间
                        */
                        long timeOut() default 60000;
                        }
                        复制

                        复制


                        3.3.2 AOP解决思路


                        Spring AOP相对与拦截器来说提供了更好的参数支持,所以我们能够更加全面的进行缓存操作。Aop中有前置通知、后置通知、返回通知、异常通知、环绕通知这几种,具体的区别就不在这里仔细讲解了,关注后续我的文档吧,我会写一个专门介绍Aop的文章。

                        这里我们就选用环绕通知,因为一个环绕通知就完全解决我们的缓存问题。使得缓存面可以缩小到每个方法上。


                        1. 实现缓存拦截的切入点 -- 注解方法/类
                          我们可以直接在AOP中配置切入点,我们使用的是通过注解来判断是否缓存及其缓存策略,恰好AOP同样支持。
                          如果我们需要对整个类进行拦截缓存,我们的AOP同样可以完美实现。(我的DEMO中就不再细说了,我只说一下方法上注解,关于注解放到类上自己琢磨一下,道理都是一样的)
                          所以说,我们通过AOP来绑定具体的拦截方法

                        2. 实现缓存 -- 环绕通知
                          AOP面向切面编程是非常灵活的,我就特别喜欢环绕通知。
                          选择环绕通知因为:1、一个方法可以满足我们的现在做缓存的需求;2、方法执行前后可控;3、可获取更多的参数,包括但不局限于目标方法、形参、实参、目标类等;4、拥有更全面的参数就可以至此更全面的Spel表达式;5、可直接获取方法返回值;等等
                          我们可以在执行方法前判断是否存在缓存,不存在缓存我们再继续执行方法,否则直接返回Redis中的缓存数据了。

                        3. 缓存灵活性 -- 注解变量及其Spel表达式
                          像CacheAble一样支持Spel表达式其实就是为了满足更多的业务需求。比如自定义缓存key、设置不同的缓存时间、设置缓存条件和不缓存条件、设置更新缓存条件等等。所以这里需要使用注解中的一些东西去动态的判断缓存逻辑。
                          我先举个例子:使用spel自定义缓存key。如果有兴趣,可以根据这个继续扩展。


                        3.3.3 具体实现


                        逻辑很简单:

                        1. 环绕通知前, 解析缓存Key, 判断Redis中是否存在缓存

                        2. 不存在缓存就执行目标方法

                        3. 获取到方法执行结果, 进行缓存

                        4. 返回此次结果

                          @Aspect
                          @Component
                          public class CacheAdvice {
                          /** 用来解析Spel表达式, 这个是我自己实现的一个类,下面会具体详解 */
                          private CacheOperatorExpression cacheOperatorExpression = new CacheOperatorExpression();
                          /** Redis */
                          private final StringRedisTemplate redisTemplate;
                          @Autowired
                          public CacheAdvice(StringRedisTemplate redisTemplate) {
                          this.redisTemplate = redisTemplate;
                          }
                          @Pointcut(value = "execution(* com.nouser..*.*(..))")
                          public void cachePointcut() { }
                          /**
                          * 我们的切入点就是含有@UseAopCache注解的方法,@annotation里填的是对应的参数的名字,Aop会自动封装。
                          * 当然,我们也可以使用@annotation(com.nouser.config.annotations.UseAopCache), 这样的话, 注解需要我们自己从joinPoint中解析。
                          * 同样支持使用@Pointcut(value = "@annotation(com.nouser.config.annotations.UseAopCache)定义切面。
                          * 我这里也定义了一个切面cachePointcut(), 取了并的关系, 是为了防止注解越界吧, 万一引用的包中存在同名的注解呢.
                          */
                          @Around(value = "cachePointcut() && @annotation(useAopCache)")
                          public Object aroundAdvice(ProceedingJoinPoint joinPoint, UseAopCache useAopCache) throws Throwable {
                          String keySpel = useAopCache.customKey();
                          //获取Redis缓存Key
                          String key = getRedisKey(joinPoint, keySpel);
                          //读取redis数据缓存数据
                          String result = redisTemplate.opsForValue().get(key);
                          if(StringUtils.isNoneBlank(result)){
                          //存在缓存结果, 将缓存的json转换成Object返回
                          return JsonUtils.parseObject4G(result);
                          }
                          //不存在缓存数据,执行方法, 获取结果, 再放入Redis中
                          Object returnObject = joinPoint.proceed();
                          //这里我没有对null数据进行缓存, 也可以在注解中设置对应的不缓存策略
                          if(returnObject == null){
                          return returnObject;
                          }
                          // 转换结果为Json
                          String cacheJson = JsonUtils.toGJsonString(returnObject);
                          // 将Json缓存到Redis, 不要忘记重注解中获取缓存时间, 设置Redis的key过期时间
                          redisTemplate.opsForValue().set(key, cacheJson, useAopCache.timeOut(), TimeUnit.MILLISECONDS);
                          return returnObject;
                          }
                          /**
                          * 从joinPoint中获取方法的上下文环境,然后从Spel表达式中解析出key
                          */
                          private String getRedisKey(ProceedingJoinPoint joinPoint, String keySpel) {
                          if (StringUtils.isNoneBlank(keySpel)) {
                          return cacheOperatorExpression.generateKey(keySpel, joinPoint);
                          }
                          return defaultKey(joinPoint);
                          }
                          /**
                          * 如果没有在注解的customKey()中设置Spel表达式, 我们总不能报错吧, 这里提供一个默认的Key, 数据都冲joinPoint中获取
                          * packageName + ':' + methodName + '#' + param
                          * 为防止param中存在特殊字符, 这里之保留[a-zA-Z0-9:#_.]
                          */
                          private String defaultKey(ProceedingJoinPoint joinPoint) {
                          StringBuilder key = new StringBuilder();
                          String className = joinPoint.getTarget().getClass().getName();
                          MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                          Method method = signature.getMethod();
                          String methodName = method.getName();
                          Object[] args = joinPoint.getArgs();
                          key.append(className).append(":").append(methodName).append("#");
                          if (args != null && args.length > 0) {
                          for (Object arg : args) {
                          key.append(arg).append("#");
                          }
                          }
                          return key.toString().replaceAll("[^a-zA-Z0-9:#_.]", "");
                          }
                          }
                          复制


                          3.3.4 Spel表达式解析(简单介绍一下, 具体请关注以后的博客)


                          Spel表达式(赶快画重点了, 这是个非常新奇的东西, 会有很多小妙用的), 全称:Spring Expression Language, 类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用。

                          我们主要是对注解中的自定义key进行解析, 生成缓存真正key。解析Spel表达式主要需要两个参数:解析器和上下文环境。

                          解析器:org.springframework.expression.spel.standard.SpelExpressionParser

                          上下文环境我看网上大多直接使用的StandardEvaluationContext, 但是我们在这个注解中主要是相关方法的解析, 所以建议使用StandardEvaluationContext的子类MethodBasedEvaluationContext。在Spring中解析CacheAble注解中的key同样是使用MethodBasedEvaluationContext的子类。

                          MethodBasedEvaluationContext在添加上下文环境的变量时,使用了懒加载, 当我们注解中的key不使用参数时,就不再添加上下文的变量,在使用的时候才去进行懒加载.而且相对于网上的一些实现, 官方实现更加靠谱. 也更加全面.

                          我对MethodBaseEvaluationContent简单做了一层封装,注释也很详细,有一些需要注意的东西就看看代码吧. 代码如下:

                          复制
                            /**
                            * 解析Spel表达式
                            */
                            @Component
                            public class CacheOperatorExpression {
                            /** 这个是Spring 提供的一个方法, 为了获取程序在运行中获取方法的实参 */
                            private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
                            /** 这里对targetMethod做了一个缓存, 防止每次都去解析重新获取targetMethod */
                            private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
                            /** Spel的解析器 */
                            private SpelExpressionParser parser;
                            /** 构造 */
                            public CacheOperatorExpression() {
                            this.parser = new SpelExpressionParser();
                            }
                            /** 构造 */
                            public CacheOperatorExpression(SpelExpressionParser parser) {
                            this.parser = parser;
                            }
                            public SpelExpressionParser getParser(SpelExpressionParser parser) {
                            return this.parser;
                            }
                            private ParameterNameDiscoverer getParameterNameDiscoverer() {
                            return this.parameterNameDiscoverer;
                            }
                            /** 这里创建获取对应的上下文环境 */
                            public EvaluationContext createEvaluationContext(Method method, Object[] args, Object target, Class<?> targetClass, Method targetMethod) {
                            /*
                            * rootObject,MethodBasedEvaluationContext的一个参数,可以为null,但是如果为null, 在StandardEvaluationContext构造中会设置rootObject = new TypedValue(rootObject)也就是rootObject = TypedValue.NULL;
                            * 这时我们在Spel表达式中就不能使用#root.xxxx获取对应的值.
                            * 为了能够使用#root我自定义了一个CacheRootObject
                            */
                            CacheRootObject rootObject = new CacheRootObject(method, args, target, targetClass);
                            return new MethodBasedEvaluationContext(rootObject, targetMethod, args, getParameterNameDiscoverer());
                            }


                            /**
                            * 解析 spel 表达式
                            * @return 执行spel表达式后的结果
                            */
                            public <T> T parseSpel(String spel, Method method, Object[] args, Object target, Class<?> targetClass, Method targetMethod, Class<T> conversionClazz) {
                            EvaluationContext context = createEvaluationContext(method, args, target, targetClass, targetMethod);
                            return this.parser.parseExpression(spel).getValue(context, conversionClazz);
                            }


                            public String generateKey(String spel, ProceedingJoinPoint joinPoint) {
                            Object[] args = joinPoint.getArgs();
                            Object target = joinPoint.getTarget();
                            Class<?> targetClass = joinPoint.getTarget().getClass();
                            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                            Method method = signature.getMethod();
                            Method targetMethod = getTargetMethod( method, targetClass);
                            return parseSpel(spel, method, args, target, targetClass, targetMethod, String.class);
                            }
                            /**
                            * 获取targetMethod
                            * TargetMethod和Method?
                            * 我们使用joinPoint获取的Method可能是一个接口方法,也就是我们把Aop的切点放在了接口上或接口的方法上。所以我们需要获取到运行的对应Class上的此方法。
                            * eg: 我们获取的{@code PersonBehavior.eatFood()}的Class可能是{@code ChildBehavior}或者{@code DefaultPersonBehavior}的. 他们都会对eatFood()进行覆盖,
                            * 而如果切点放在Class PersonBehavior上, 那么通过joinPoint获取的Method实际并不是程序调用的Method。
                            * 所以我们需要通过程序调用的Class去反解析出真正调用的Method就是targetMethod.
                            */
                            private Method getTargetMethod(Method method, Class<?> targetClass) {
                            AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
                            Method targetMethod = this.targetMethodCache.get(methodKey);
                            if (targetMethod == null) {
                            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
                            if (targetMethod == null) {
                            targetMethod = method;
                            }
                            this.targetMethodCache.put(methodKey, targetMethod);
                            }
                            return targetMethod;
                            }


                            }
                            // ############################################
                            /**
                            * 自定义的RootObject, 让spel表达式至此#root参数, #root就是对应这个Object, #root.method就是对应这个类中的Method
                            */
                            public class CacheRootObject {
                            private final Method method;
                            private final Object[] args;
                            private final Object target;
                            private final Class<?> targetClass;
                            public CacheRootObject( Method method, Object[] args, Object target, Class<?> targetClass) {
                            this.method = method;
                            this.target = target;
                            this.targetClass = targetClass;
                            this.args = args;
                            }
                            /* get set方法*/
                            }
                            复制

                            复制


                            3.3.5 反馈


                            毕竟是我们自己实现的代码, 没有千锤百练谁也不能说完美. 请问世间是否存在完美的代码,除了HelloWorld只求产品不改需求.

                            1. 麻烦!!! 不管多少代码, 不管自己的逻辑有多么完美, 但是还是要自己写啊, 万一改需求了这个缓存逻辑行不通了呢, 程序员事情很多的好吧.

                            2. 懒, 谁也想不起来那么多的业务逻辑, 老板也不会给你太多时间让你去开发个灵活的“框架??”

                            3. 有没有更好的方法呢, 就那种配置配置就能使用的那种, 不用担心出现bug的那种, 即使出现了bug能推出去的那种, 特别特别好使用的那种, 反正就不是我写的代码bug就不是我的那种. 反正老板也是只看结果.

                            4. 如果你使用的是SpringBoot, 还真有.


                            4、SpringBoot整合Redis缓存


                            Redis那么一个经典的NoSql数据库,SpringBoot缓存肯定也对它进行支持. SpringBoot的缓存功能已经为我们提供了使用Redis做缓存.


                            4.1 引入环境


                            上面我们已经引入了Redis,这里我们还需要引入SpringBoot的Cache包


                            复制
                                      <dependency>
                              <groupId>org.springframework.boot</groupId>
                              <artifactId>spring-boot-starter-cache</artifactId>
                              </dependency>
                              复制

                              复制


                              4.2 查找SpringBoot对Redis缓存的支持


                              随便百度一下或者google一下都能找到SpringBoot对各种缓存的支持都是实现接口:org.springframework.cache.annotation.CachingConfigurer

                              注释上同样写的大概意思就是:我们使用org.springframework.context.annotation.Configuration配置的实现类, 能够为注解@EnableCaching实现缓存解析和缓存管理器, 所以, 我们只需要实现此接口, 就可以直接使用@EnableCaching注解进行缓存管理.

                              我们可在Ide上查看CachingConfigurer接口的子类可以看到好像并没有关于Redis的实现。所以我们就需要手动去实现这个接口了。较好的是CachingConfigurere接口中注释的非常清楚。大家可以看一下源码


                              复制
                                /**
                                * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration
                                * Configuration} classes annotated with @{@link EnableCaching} that wish or need to
                                * specify explicitly how caches are resolved and how keys are generated for annotation-driven
                                * cache management. Consider extending {@link CachingConfigurerSupport}, which provides a
                                * stub implementation of all interface methods.
                                *
                                * <p>See @{@link EnableCaching} for general examples and context; see
                                * {@link #cacheManager()}, {@link #cacheResolver()} and {@link #keyGenerator()}
                                * for detailed instructions.
                                */
                                public interface CachingConfigurer {
                                /** 缓存管理器 */
                                @Nullable
                                CacheManager cacheManager();
                                /** 缓存解析器,注解上说是一个比缓存管理器更加强大的实现. 他和cacheManager互斥, 只能存在一个, 两个都有的话会报异常.
                                * 这次我使用的是CacheManager, 因为之前我尝试CacheResolver的时候使用SimpleCacheResolver然后在CacheManager中自定义的缓存过期时间不生效.然后没有研究了, 下次研究完我再补上 */
                                @Nullable
                                CacheResolver cacheResolver();
                                /** key序列化方式 */
                                @Nullable
                                KeyGenerator keyGenerator();
                                /** 错误处理 */
                                @Nullable
                                    CacheErrorHandler errorHandler();
                                }
                                复制

                                复制


                                4.3 缓存管理器CacheManager


                                虽然SpringBoot没有给我们实现CachingConfigurer, 但是缓存管理器是已经帮助我们实现了的。我们引入了cache包后,会存在一个RedisCacheManager, 我们的缓存管理器就使用它来实现.


                                RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory).build();
                                复制


                                我们使用RedisCacheManager提供的builder静态方法去创建, 需要参数链接工厂, 即需要一个能够创建Redis链接的对象, 这个对象存在于Spring容器中, 我们直接通过注解获取即可。

                                我们使用redisCacheConfiguration来做一些配置, 比如key的前缀、key/value的序列化方式、缓存名称和对应的缓存时间等等。redis KeyValue的序列化方式:key就选用的StringRedisSerializer,而value我们大多都会选择使用json. 这些序列化方式都是实现的RedisSerializer


                                复制
                                  /**
                                  * 自定义Redis缓存管理器
                                  * 可以参考{@link RedisCacheConfiguration}
                                  * 设置过期时间可参考:{@link RedisCacheConfiguration#entryTtl(java.time.Duration)}的return值
                                  */
                                  public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
                                  RedisCacheConfiguration defaultCacheConf = RedisCacheConfiguration.defaultCacheConfig()
                                  //设置缓存key的前缀生成方式
                                  .computePrefixWith(cacheName -> profilesActive + "-" + cacheName + ":" )
                                  // 设置key的序列化方式
                                  .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                                  // 设置value的序列化方式
                                  .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                                  // 不缓存null值,但是如果存在空值,org.springframework.cache.interceptor.CacheErrorHandler.handleCachePutError会异常:
                                  // 异常内容: Cache 'cacheNullTest' does not allow 'null' values. Avoid storing null via '@Cacheable(unless="#result == null")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.
                                  // .disableCachingNullValues()
                                  // 默认60s缓存
                                  .entryTtl(Duration.ofSeconds(60));


                                  //设置缓存时间,使用@Cacheable(value = "xxx")注解的value值
                                  CacheTimes[] times = CacheTimes.values();
                                  Set<String> cacheNames = new HashSet<>();
                                  //设置缓存时间,使用@Cacheable(value = "user")注解的value值作为key, value是缓存配置,修改默认缓存时间
                                  ConcurrentHashMap<String, RedisCacheConfiguration> configMap = new ConcurrentHashMap<>();
                                  for (CacheTimes time : times) {
                                  cacheNames.add(time.getCacheName());
                                  configMap.put(time.getCacheName(), defaultCacheConf.entryTtl(time.getCacheTime()));
                                  }


                                  //需要先初始化缓存名称,再初始化其它的配置。
                                  RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                                  //设置缓存name
                                  .initialCacheNames(cacheNames)
                                  //设置缓存配置
                                  .withInitialCacheConfigurations(configMap)
                                  //设置默认配置
                                  .cacheDefaults(defaultCacheConf)
                                  //说是与事务同步,但是具体还不是很清晰
                                  .transactionAware()
                                  .build();


                                  return redisCacheManager;
                                  }
                                  复制

                                  复制


                                  4.4 异常处理


                                  我们在看CachingConfigurer时, 会发现我们会获取一个CacheErrorHandler的类, 这个类就是对缓存过程中出现异常时对异常进行操作的对象.

                                  CacheErrorHandler是一个接口,这个接口提供了对: 获取缓存异常、设置缓存异常、解析缓存异常、清除缓存异常 这五种异常的处理.

                                  官方给出了一个默认实现SimpleCacheErrorHandler,默认实现就像名称一样很简单, 把异常抛出, 不做任何处理, 但是如果抛出异常,就会对我们的业务逻辑存在影响。

                                  eg:我们的缓存Redis突然宕机, 如果仅仅因为缓存宕机就导致服务异常不可用那就太尴尬了,所以不建议使用默认的SimpleCacheErrorHandler, 所以我建议自己去实现这个, 我这里选择了打日志的方式处理. 即使缓存不可用,仍然可以走正常的逻辑去获取. 可能这会对下游服务造成压力,这就看你的实现了.


                                  复制


                                    /**
                                    * 异常处理接口
                                    */
                                    public interface CacheErrorHandler {


                                    void handleCacheGetError(RuntimeException exception, Cache cache, Object key);


                                    void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value);


                                    void handleCacheEvictError(RuntimeException exception, Cache cache, Object key);


                                    void handleCacheClearError(RuntimeException exception, Cache cache);
                                    }
                                    /** 官方默认实现 */
                                    public class SimpleCacheErrorHandler implements CacheErrorHandler {
                                    @Override
                                    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                                    throw exception;
                                    }
                                    @Override
                                    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) {
                                    throw exception;
                                    }
                                    @Override
                                    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                                    throw exception;
                                    }
                                    @Override
                                    public void handleCacheClearError(RuntimeException exception, Cache cache) {
                                    throw exception;
                                    }
                                    }


                                    /**
                                    * 我的实现, 效果可能和官方实现相反, 但是都没有对异常进行处理.
                                    */
                                    protected class CustomLogErrorHandler implements CacheErrorHandler{
                                    @Override
                                    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                                    String format = String.format("RedisCache Get Exception:%s, cache customKey:%s, key:%s", exception.getMessage(), cache.getName(), key.toString());
                                    logger.error(format, exception);
                                    }
                                    @Override
                                    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                                    String format = String.format("RedisCache Put Exception:%s, cache customKey:%s, key:%s, value:%s", exception.getMessage(), cache.getName(), key.toString(), JSON.toJSONString(value));
                                    logger.error(format, exception);
                                    }
                                    @Override
                                    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                                    String format = String.format("RedisCache Evict Exception:%s, cache customKey:%s, key:%s", exception.getMessage(), cache.getName(), key.toString());
                                    logger.error(format, exception);
                                    }
                                    @Override
                                    public void handleCacheClearError(RuntimeException exception, Cache cache) {
                                    String format = String.format("RedisCache Clear Exception:%s, cache customKey:%s", exception.getMessage(), cache.getName());
                                    logger.error(format, exception);
                                    }
                                    }
                                    复制

                                    复制


                                    4.5 完整代码


                                    上面说了那么多只是为了让大家好理解而已, 在SpringBoot项目中只需要创建一个下面的类即可.


                                    这个依赖Redis的配置, 如何配置Redis在上面


                                    复制
                                      @Configuration
                                      @EnableCaching
                                      public class RedisCacheConfig extends CachingConfigurerSupport {
                                      private static final Logger logger = LoggerFactory.getLogger(RedisCacheConfig.class);
                                      /**
                                      * redis
                                      */
                                      @Autowired
                                      private RedisConnectionFactory connectionFactory;
                                      /** 我自定义了一个前缀, 去区分环境 */
                                      @Value("${com.nouser.profiles.active}")
                                      private String profilesActive;


                                      /**
                                      * 有点问题#######################自定义过期时间不生效
                                      @Bean important!
                                      @Override
                                      public CacheResolver cacheResolver() {
                                      configure and return CacheResolver instance
                                      return new SimpleCacheResolver(cacheManager(connectionFactory));
                                      }
                                      */


                                      /**
                                      * 设置com.example.demo.cache.RedisConfig#cacheResolver()就不在是用这个了
                                      */
                                      @Bean // important!
                                      @Override
                                      public CacheManager cacheManager() {
                                      // configure and return CacheManager instance
                                      return cacheManager(connectionFactory);
                                      }


                                      /**
                                      * 默认的key生成策略, 包名 + 方法名。建议使用Cacheable注解时使用Spel自定义缓存key.
                                      */
                                      @Bean
                                      @Override
                                      public KeyGenerator keyGenerator() {
                                      return (o, method, params) -> o.getClass().getName() + ":" + method.getName();
                                      }


                                      /**
                                      * 设置读写缓存异常处理
                                      */
                                      @Bean
                                      @Override
                                      public CacheErrorHandler errorHandler() {
                                      logger.error("handler redis cache Exception.");
                                      return new CustomLogErrorHandler();
                                      }
                                      /**
                                      * 自定义Redis缓存管理器
                                      * 可以参考{@link RedisCacheConfiguration}
                                      * 设置过期时间可参考:{@link RedisCacheConfiguration#entryTtl(java.time.Duration)}的return值
                                      */
                                      public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
                                      RedisCacheConfiguration defaultCacheConf = RedisCacheConfiguration.defaultCacheConfig()
                                      //设置缓存key的前缀生成方式
                                      .computePrefixWith(cacheName -> profilesActive + "-" + cacheName + ":" )
                                      // 设置key的序列化方式
                                      .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                                      // 设置value的序列化方式
                                      .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                                      // 不缓存null值,但是如果存在空值,org.springframework.cache.interceptor.CacheErrorHandler.handleCachePutError会异常:
                                      // 异常内容: Cache 'cacheNullTest' does not allow 'null' values. Avoid storing null via '@Cacheable(unless="#result == null")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.
                                      // .disableCachingNullValues()
                                      // 默认60s缓存
                                      .entryTtl(Duration.ofSeconds(60));


                                      //设置缓存时间,使用@Cacheable(value = "xxx")注解的value值
                                      CacheTimes[] times = CacheTimes.values();//我把过期时间分阶段做了一个enum类, 然后遍历, 后续使用时也使用这个enum去设置时间
                                      Set<String> cacheNames = new HashSet<>();
                                      //设置缓存时间,使用@Cacheable(value = "user")注解的value值作为key, value是缓存配置,修改默认缓存时间
                                      ConcurrentHashMap<String, RedisCacheConfiguration> configMap = new ConcurrentHashMap<>();
                                      for (CacheTimes time : times) {
                                      cacheNames.add(time.getCacheName());
                                      configMap.put(time.getCacheName(), defaultCacheConf.entryTtl(time.getCacheTime()));
                                      }


                                      //需要先初始化缓存名称,再初始化其它的配置。
                                      RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                                      //设置缓存name
                                      .initialCacheNames(cacheNames)
                                      //设置缓存配置
                                      .withInitialCacheConfigurations(configMap)
                                      //设置默认配置
                                      .cacheDefaults(defaultCacheConf)
                                      //说是与事务同步,但是具体还不是很清晰
                                      .transactionAware()
                                      .build();


                                      return redisCacheManager;
                                      }
                                      /**
                                      * 因为默认key都是字符串,就使用默认的字符串序列化方式,没毛病
                                      */
                                      private RedisSerializer<String> keySerializer() {
                                      return new StringRedisSerializer();
                                      }


                                      /**
                                      * value值序列化方式
                                      * 使用Jackson2Json的方式存入redis
                                      * ** 注意,要缓存的类型,必须有 "默认构造(无参构造)" ,否则从json2class时会报异常,提升没有默认构造。
                                      */
                                      private GenericJackson2JsonRedisSerializer valueSerializer() {
                                      GenericJackson2JsonRedisSerializer redisSerializer = new GenericJackson2JsonRedisSerializer();
                                      return redisSerializer;
                                      }


                                      /**
                                      * 其他集合等转换正常,但是不知道为啥啊RespResult转换异常
                                      * java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.example.demo.util.RespResult
                                      */
                                      private FastJsonRedisSerializer valueSerializerFastJson(){
                                      FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
                                      return fastJsonRedisSerializer;
                                      }




                                      /**
                                      * 自定义异常处理
                                      */
                                      protected class CustomLogErrorHandler implements CacheErrorHandler{
                                      @Override
                                      public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                                      String format = String.format("RedisCache Get Exception:%s, cache customKey:%s, key:%s", exception.getMessage(), cache.getName(), key.toString());
                                      logger.error(format, exception);
                                      }
                                      @Override
                                      public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                                      String format = String.format("RedisCache Put Exception:%s, cache customKey:%s, key:%s, value:%s", exception.getMessage(), cache.getName(), key.toString(), JSON.toJSONString(value));
                                      logger.error(format, exception);
                                      }
                                      @Override
                                      public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                                      String format = String.format("RedisCache Evict Exception:%s, cache customKey:%s, key:%s", exception.getMessage(), cache.getName(), key.toString());
                                      logger.error(format, exception);
                                      }
                                      @Override
                                      public void handleCacheClearError(RuntimeException exception, Cache cache) {
                                      String format = String.format("RedisCache Clear Exception:%s, cache customKey:%s", exception.getMessage(), cache.getName());
                                      logger.error(format, exception);
                                      }
                                      }


                                      }
                                      复制

                                      复制


                                      4.6 使用: @Cacheable


                                      配置好了, 我们如何使用呢?


                                      我们现在是使用的SpringBoot缓存整合Redis, 所以我们只需要使用注解@Cacheable, 我们先看一下Cacheable注解, 然后说一下它如何使用.


                                      复制
                                        /** 这里只贴代码, 注释自己去ide看吧, 源码上的注释挺全的 */
                                        @Target({ElementType.TYPE, ElementType.METHOD})
                                        @Retention(RetentionPolicy.RUNTIME)
                                        @Inherited
                                        @Documented
                                        public @interface Cacheable {
                                        /** 缓存名,和前面我们设置缓存管理器时初始化缓存名称和配置一一对应, 如果为空, 则取默认配置 */
                                        @AliasFor("cacheNames")
                                        String[] value() default {};
                                        @AliasFor("value")
                                        String[] cacheNames() default {};
                                        /** 设置缓存的key, 每个缓存key是唯一的, 我们使用Redis缓存, 那么它生成的结果就是我们的Redis Key */
                                        String key() default "";
                                        /** 指定key生成策略*/
                                        String keyGenerator() default "";
                                        /** 指定缓存管理器 */
                                        String cacheManager() default "";
                                        /** 制定解析器 */
                                        String cacheResolver() default "";
                                        /** 是否走缓存逻辑, 缓存前进行判定, 是否走缓存逻辑, 支持Spel表达式, 如果返回false, 将会跳过缓存逻辑 */
                                        String condition() default "";
                                        /** 是否进行缓存, 这个是在执行目标方法后进行判断, 支持Spel表达式, 如果为true, 将不会对结果进行缓存 */
                                        String unless() default "";
                                        /** 是否使用同步 */
                                        boolean sync() default false;
                                        }
                                        复制

                                        复制


                                        单独说一下sync(), 如果我们设置sync为true, 那么我们执行到获取缓存的get方法时, 这个方法是访问的加锁的同步方法,只能同步调用,但是保证了缓存失效时不会全部请求都到下游服务请求。
                                        注解也非常清楚:参考org.springframework.data.redis.cache.RedisCache.get, 可以自己打断点试一试, 反正这个不建议使用, 除非业务不影响业务的且需要保证下游服务的前提下.
                                        复制


                                        关于Cacheable注解的使用.....我举几个例子吧


                                        复制
                                          /**
                                          * 缓存key = packageName + ":" + methodName + "#" + #name + "#" + #id
                                          * 如果方法结果为null 或长度 小于1 则不缓存此结果
                                          * 参数useCache = true 的时候才走缓存逻辑,
                                          */
                                          @Cacheable(value = "xxxx",
                                          key = "(#root.targetClass.getName() + ':' + #root.methodName + '#' + #name + '#' + #id).replaceAll('[^0-9a-zA-Z:#._]', '')",
                                          unless = "#result == null || #result.size() < 1",
                                          condition = "#useCache"
                                          )
                                          public List<String> cache01(String name, String id, boolean useCache){}
                                          复制

                                          我的语雀博客,点击阅读原文直达哦

                                          https://www.yuque.com/cnkaile/blog


                                          点击阅读原文关注我的语雀文档哦

                                          阅读原文提升阅读体验啦

                                          文章转载自乐吧了吧,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                          评论