vlambda博客
学习文章列表

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

如何使用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=6379spring.redis.timeout=5000# Redis服务器连接密码(默认为空)spring.redis.password=zxxxspring.redis.lettuce.pool.max-active=8spring.redis.lettuce.pool.min-idle=1spring.redis.lettuce.pool.max-idle=8spring.redis.lettuce.pool.max-wait=500msspring.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是否存在缓存,如果存在缓存,则直接返回其值即可。


代码如下:


/** * 缓存拦截器 */@Componentpublic 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(...)*/@RestControllerAdvicepublic 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=6379spring.redis.timeout=500# Redis服务器连接密码(默认为空)spring.redis.password=xxxspring.redis.lettuce.pool.max-active=8spring.redis.lettuce.pool.min-idle=1spring.redis.lettuce.pool.max-idle=8spring.redis.lettuce.pool.max-wait=500msspring.redis.lettuce.shutdown-timeout=100ms


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


@Configurationpublic 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})@Documentedpublic @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@Componentpublic 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表达式 */@Componentpublic 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@EnableCachingpublic 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@Documentedpublic @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){}


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