预期读者

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

文章内容说明

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

快速入门

  1. maven 加入 jar 包

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

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

    @EnableCaching
  4. @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<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 表达式,可以使用 + 号来拼接参数,常量使用 "" 来包含,更多例子

@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

spring-data-redis-cache 使用及源码走读的更多相关文章

  1. Spring Data Redis—Pub/Sub(附Web项目源码)

    一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...

  2. Spring Data Redis—Pub/Sub(附Web项目源码) (转)

    一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...

  3. Redis与Spring Data Redis

    1.Redis概述 1.1介绍 官网:https://redis.io/ Redis是一个开源的使用ANSIC语言编写.支持网络.可基于内存 亦可持久化的日志型.Key-Value型的高性能数据库. ...

  4. spring mvc Spring Data Redis RedisTemplate [转]

    http://maven.springframework.org/release/org/springframework/data/spring-data-redis/(spring-data包下载) ...

  5. Spring Data Redis简介以及项目Demo,RedisTemplate和 Serializer详解

    一.概念简介: Redis: Redis是一款开源的Key-Value数据库,运行在内存中,由ANSI C编写,详细的信息在Redis官网上面有,因为我自己通过google等各种渠道去学习Redis, ...

  6. Spring Data Redis 2.x 中 RedisConfiguration 类的新编写方法

    在 Spring Data Redis 1.x 的时候,我们可能会在项目中编写这样一个RedisConfig类: @Configuration @EnableCaching public class ...

  7. Spring Data Redis入门示例:字符串操作(六)

    Spring Data Redis对字符串的操作,封装在了ValueOperations和BoundValueOperations中,在集成好了SPD之后,在需要的地方引入: // 注入模板操作实例 ...

  8. 使用Spring Data Redis时,遇到的几个问题

    需求: 1,保存一个key-value形式的结构到redis 2,把一个对象保存成hash形式的结构到redis 代码如下: // 保存key-value值         pushFrequency ...

  9. Spring Data Redis 让 NoSQL 快如闪电 (1)

    [编者按]本文作者为 Xinyu Liu,详细介绍了 Redis 的特性,并辅之以丰富的用例.在本文的第一部分,将重点概述 Redis 的方方面面.文章系国内 ITOM 管理平台 OneAPM 编译呈 ...

  10. Spring Data Redis示例

    说明 关于Redis:一个基于键值对存储的NoSQL内存数据库,可存储复杂的数据结构,如List, Set, Hashes. 关于Spring Data Redis:简称SDR, 能让Spring应用 ...

随机推荐

  1. zoj - 4059 Kawa Exam scc + dsu

    Kawa Exam 题解:先scc把图变成树, 然后对于这若干棵树在进行dsu的操作. dsu就是先找到最大的子树放在一边,然后先处理小的子树,最后处理大的子树.无限递归. 重要的一点就是 是否重新添 ...

  2. poj 1797Heavy Transportation(dijkstra变形)

    题目链接:http://poj.org/problem?id=1797 题意:有n个城市,m条道路,在每条道路上有一个承载量,现在要求从1到n城市最大承载量,而最大承载量就是从城市1到城市n所有通路上 ...

  3. 工作中遇到的99%SQL优化,这里都能给你解决方案(三)

    -- 示例表 CREATE TABLE `employees` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL ...

  4. java读写文件IO

    package Common.readFile; import Common.tool.User; import com.fasterxml.jackson.databind.ObjectMapper ...

  5. Android的消息循环与Handler机制理解

    一.概念 1.事件驱动型 什么是事件驱动?就是有事了才去处理,没事就躺着不动.假如把用户点击按钮,滑动页面等这些都看作事件,事件产生后程序就执行相应的处理方法,就是属于事件驱动型. 2.消息循环 把需 ...

  6. group by语法

    group by 用法解析 group by语法可以根据给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表. SELECT子句中的列名必须为分组列或列函数.列函数对于GROUP BY子 ...

  7. Python集训营45天—Day03

    目录 1. 分支结构 1.1 初步介绍 1.2 使用案例 1.3 练习 2.循环结构 1.1 初步介绍 1.2 使用案例 1. 分支结构 1.1 初步介绍 至今,我们所写的Python代码都是顺序执行 ...

  8. FaceBook快捷登入

    关于集成FaceBook快捷登入,我上回做了个最简单的版本,所有Web端通用,在这边共享下,有更好的解决方案的,麻烦评论留个地址,有不妥之处请指正. 首先,我们先加载Facebook的Js windo ...

  9. CentOS7 常用命令

    常用命令 1.文件与目录操作 命令 解析 cd /home 进入 ‘/home’ 目录 cd .. 返回上一级目录 cd ../.. 返回上两级目录 cd - 返回上次所在目录 cp file1 fi ...

  10. NPOI导出数值格式设置(我是保留四位小数,不足补0)

    看了网上好多帖子,都是保留两位小数的,写法是: HSSFDataFormat.GetBuiltinFormat("0.00"); 于是想四位小数,就是多加两个00,变成: HSSF ...