预期读者 准备使用 spring 的 data-redis-cache 的同学 了解 @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的使用 深入理解 data-redis-cache 的实现原理 文章内容说明 如何使用 redis-cache 自定义 keyGenerator 和过期时间 源码解读 自带缓存机制的不足 快速入门 maven 加入 jar 包 org.springframework.boot spring-boot-starter-data-redis 配置 redis spring.redis.host=127.0.0.1 开启 redis-cache @EnableCaching @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的功能 @Cacheable 会查询缓存中是否有数据,如果有数据则返回,否则执行方法 @CachePut 每次都执行方法,并把结果进行缓存 @CacheEvict 会删除缓存中的内容 @Caching 相当于上面三者的综合,用于配置三者的行为 @CacheConfig 配置在类上,用于配置当前类的全局缓存配置 详细配置 经过上面的配置,就已经可以使用 redis-cache 了,但是还是有些问题需要问自己一下,比如 存储在 redis 的 key 是什么样子的,我可以自定义 key 吗 存储到 redis 的 value 是怎么序列化的 存储的缓存是多久过期 并发访问时,会不会直接穿透从而不断的修改缓存内容 过期时间,序列化方式由此类决定 RedisCacheConfiguration,可以覆盖此类达到自定义配置。默认配置为RedisCacheConfiguration.defaultCacheConfig() ,它配置为永不过期,key 为 String 序列化,并加上了一个前缀做为命名空间,value 为 Jdk 序列化,所以你要存储的类必须要实现 java.io.Serializable 。 存储的 key 值的生成由 KeyGenerator 决定,可以在各缓存注解上进行配置,默认使用的是 SimpleKeyGenerator 其存储的 key 方式为 SimpleKey [参数名1,参数名2],如果在同一个命名空间下,有两个同参数名的方法就公出现冲突导致反序列化失败。 并发访问时,确实存在多次访问数据库而没有使用缓存的情况 https://blog.csdn.net/clementad/article/details/52452119 Srping 4.3提供了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。 自定义存储 key 根据上面的说明 ,很有可能会存在存储的 key 一致而导致反序列化失败,所以需要自定义存储 key ,有两种实现办法 ,一种是使用元数据配置 key(简单但难维护),一种是全局设置 keyGenerator 使用元数据配置 key @Cacheable(key = "#vin+#name") public List testMetaKey(String vin,String name){ List vehicles = dataProvide.selectAll(); return vehicles.stream().filter(vehicle -> vehicle.getVin().equals(vin) && vehicle.getName().contains(name)).collect(Collectors.toList()); } 这是一个 spel 表达式,可以使用 + 号来拼接参数,常量使用 "" 来包含,更多例子 @Cacheable(value = "user",key = "targetClass.name + '.'+ methodName") @Cacheable(value = "user",key = "'list'+ targetClass.name + '.'+ methodName + #name ") 注意: 生成的 key 不能为空值,不然会报错误 Null key returned for cache operation 常用的元数据信息 名称 位置 描述 示例 methodName root 当前被调用的方法名 #root.methodName method root 被调用的方法对象 #root.method.name target root 当前实例 #root.target targetClass root 当前被调用方法参数列表 #root.targetClass args root 当前被调用的方法名 #root.args[0] caches root 使用的缓存列表 #root.caches[0].name Argument Name 执行上下文 方法参数数据 #user.id result 执行上下文 方法返回值数据 #result.id 使用全局 keyGenerator 使用元数据的特点是简单,但是难维护,如果需要配置的缓存接口较多的话,这时可以配置一个 keyGenerator ,这个配置配置多个,引用其名称即可。 @Bean public KeyGenerator cacheKeyGenerator() { return (target, method, params) -> { return target + method + params; } } 自定义序列化和配置过期时间 因为默认使用值序列化为 Jdk 序列化,存在体积大,增减字段会造成序列化异常等问题,可以考虑其它序列化来覆写默认序列化。 @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){ RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); // 设置过期时间为 30 天 redisCacheConfiguration.entryTtl(Duration.ofDays(30)); redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new KryoRedisSerializer())); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(customConfigs) .build(); } 个性化配置过期时间和序列化 上面的是全局配置过期时间和序列化,可以针对每一个 cacheNames 进行单独设置,它是一个 Map 配置 Map customConfigs = new HashMap<>(); customConfigs.put("cacheName1",RedisCacheConfiguration.defaultCacheConfig()); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(customConfigs) .build(); 源码走读 本源码走读只带你入门,具体的细节需要具体分析 首先不用看源码也知道这肯定是动态代理来实现的,代理目标方法,获取配置,然后增强方法功能; aop 就是干这件事的,我们自己也经常加一些注解来实现日志信息采集,其实和这个原理一致,spring-data-cache-redis 也是使用 aop 实现的。 从 @EnableCaching 开始,可以看到导入了一个选择导入配置的配置类(有点绕,就是可以自己控制导入哪些配置类),默认使用 PROXY 模式 public class CachingConfigurationSelector extends AdviceModeImportSelector PROXY 导入了如下配置类 private String[] getProxyImports() { List result = new ArrayList<>(3); result.add(AutoProxyRegistrar.class.getName()); result.add(ProxyCachingConfiguration.class.getName()); if (jsr107Present && jcacheImplPresent) { result.add(PROXY_JCACHE_CONFIGURATION_CLASS); } return StringUtils.toStringArray(result); } ProxyCachingConfiguration 重点的配置类是在这个配置类中,它配置了三个 Bean BeanFactoryCacheOperationSourceAdvisor 是 CacheOperationSource 的一个增强器 CacheOperationSource 主要提供查找方法上缓存注解的方法 findCacheOperations CacheInterceptor 它是一个 MethodInterceptor 在调用缓存方法时,会执行它的 invoke 方法 下面来看一下 CacheInterceptor 的 invoke 方法 // 关键代码就一句话,aopAllianceInvoker 是一个函数式接口,它会执行你的真实方法 execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments()); 进入 execute 方法,可以看到这一层只是获取到所有的缓存操作集合,@CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 然后把其配置和当前执行上下文进行绑定成了 CacheOperationContexts Class targetClass = getTargetClass(target); CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { Collection operations = cacheOperationSource.getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass)); } } 再进入 execute 方法,可以看到前面专门是对 sync 做了处理,后面才是对各个注解的处理 if (contexts.isSynchronized()) { // 这里是专门于 sync 做的处理,可以先不去管它,后面再来看是如何处理的,先看后面的内容 } // Process any early evictions 先做缓存清理工作 processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); // Check if we have a cached item matching the conditions 查询缓存中内容 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); // Collect puts from any @Cacheable miss, if no cached item is found 如果缓存没有命中,收集 put 请求,后面会统一把需要放入缓存中的统一应用 List cachePutRequests = new LinkedList<>(); if (cacheHit == null) { collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); } Object cacheValue; Object returnValue; // 缓存有命中并且不是 @CachePut 的处理 if (cacheHit != null && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit cacheValue = cacheHit.get(); returnValue = wrapCacheValue(method, cacheValue); } else { // Invoke the method if we don't have a cache hit 缓存没有命中,执行真实方法 returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); } // Collect any explicit @CachePuts collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // Process any collected put requests, either from @CachePut or a @Cacheable miss 把前面收集到的所有 putRequest 数据放入缓存 for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); } // Process any late evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; 看完了执行流程,现在看一下CacheInterceptor 的超类 CacheAspectSupport ,因为我可以不设置 cacheManager 就可以使用,查看默认的 cacheManager是在哪设置的 public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { // .... } BeanFactoryAware 用来获取 BeanFactory InitializingBean 用来管理 Bean 的生命周期,可以在 afterPropertiesSet后添加逻辑 SmartInitializingSingleton 实现该接口后,当所有单例 bean 都初始化完成以后, 容器会回调该接口的方法 afterSingletonsInstantiated 在 afterSingletonsInstantiated 中,果然进行了 cacheManager 的设置,从 IOC 容器中拿了一个 cacheManger setCacheManager(this.beanFactory.getBean(CacheManager.class)); 那这个 CacheManager 是谁呢 ,可以从RedisCacheConfiguration类知道答案 ,在这里面配置了一个 RedisCacheManager @Configuration @ConditionalOnClass(RedisConnectionFactory.class) @AutoConfigureAfter(RedisAutoConfiguration.class) @ConditionalOnBean(RedisConnectionFactory.class) @ConditionalOnMissingBean(CacheManager.class) @Conditional(CacheCondition.class) class RedisCacheConfiguration {} @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager .builder(redisConnectionFactory) .cacheDefaults(determineConfiguration(resourceLoader.getClassLoader())); List cacheNames = this.cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); } return this.customizerInvoker.customize(builder.build()); } 从 determineConfiguration() 方法中可以知道 cacheManager 的默认配置 最后看一下,它的切点是如何定义的,即何时会调用 CacheInterceptor 的 invoke 方法 切点的配置是在 BeanFactoryCacheOperationSourceAdvisor 类中,返回一个这样的切点 CacheOperationSourcePointcut ,覆写 MethodMatcher 中的 matchs ,如果方法上存在注解 ,则认为可以切入。 spring-data-redis-cache 的不足 尽管功能已经非常强大,但它没有解决缓存刷新的问题,如果缓存在某一时间过期 ,将会有大量的请求打进数据库,会造成数据库很大的压力。 4.3 版本在这方面做了下并发控制,但感觉比较敷衍,简单的锁住其它请求,先把数据 load 到缓存,然后再让其它请求走缓存。 后面我将自定义缓存刷新,并做一个 cache 加强控件,尽量不对原系统有太多的侵入,敬请关注 一点小推广 创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。 Excel 通用导入导出,支持 Excel 公式 博客地址:https://blog.csdn.net/sanri1993/article/details/100601578 gitee:https://gitee.com/sanri/sanri-excel-poi 使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具 博客地址:https://blog.csdn.net/sanri1993/article/details/98664034 gitee:https://gitee.com/sanri/sanri-tools-mavenhttps://www.cnblogs.com/sanri1993/p/11667128.html