spring-data-redis-cache 使用及源码走读

预期读者

  • 准备使用 spring 的 data-redis-cache 的同窗
  • 了解 @CacheConfig@Cacheable@CachePut@CacheEvict@Caching 的使用
  • 深刻理解 data-redis-cache 的实现原理

文章内容说明

  • 如何使用 redis-cache
  • 自定义 keyGenerator 和过时时间
  • 源码解读
  • 自带缓存机制的不足

快速入门

  1. maven 加入 jar 包java

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置 redisgit

    spring.redis.host=127.0.0.1
  3. 开启 redis-cacheredis

    @EnableCaching
  4. @CacheConfig@Cacheable@CachePut@CacheEvict@Caching 的功能spring

    • @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/52452119app

Srping 4.3提供了一个sync参数。是当缓存失效后,为了不多个请求打到数据库,系统作了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。

自定义存储 key

根据上面的说明 ,颇有可能会存在存储的 key 一致而致使反序列化失败,因此须要自定义存储 key ,有两种实现办法 ,一种是使用元数据配置 key(简单但难维护),一种是全局设置 keyGeneratormaven

使用元数据配置 key

@Cacheable(key = "#vin+#name")
    public List<Vehicle> testMetaKey(String vin,String name){
        List<Vehicle> vehicles = dataProvide.selectAll();
        return vehicles.stream().filter(vehicle -> vehicle.getVin().equals(vin) && vehicle.getName().contains(name)).collect(Collectors.toList());
    }

这是一个 spel 表达式,可使用 号来拼接参数,常量使用 "" 来包含,更多例子ide

@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<String, RedisCacheConfiguration> 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<EnableCaching>

PROXY 导入了以下配置类

private String[] getProxyImports() {
    List<String> 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

BeanFactoryCacheOperationSourceAdvisorCacheOperationSource 的一个加强器

CacheOperationSource 主要提供查找方法上缓存注解的方法 findCacheOperations

CacheInterceptor 它是一个 MethodInterceptor 在调用缓存方法时,会执行它的 invoke 方法

下面来看一下 CacheInterceptorinvoke 方法

// 关键代码就一句话,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<CacheOperation> 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<CachePutRequest> 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<String> cacheNames = this.cacheProperties.getCacheNames();
    if (!cacheNames.isEmpty()) {
        builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
    }
    return this.customizerInvoker.customize(builder.build());
}

determineConfiguration() 方法中能够知道 cacheManager 的默认配置

最后看一下,它的切点是如何定义的,即什么时候会调用 CacheInterceptorinvoke 方法

切点的配置是在 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-maven

相关文章
相关标签/搜索