spring-data-redis-cache 使用及源码走读
预期读者
- 准备使用 spring 的 data-redis-cache 的同学
- 了解
@CacheConfig
,@Cacheable
,@CachePut
,@CacheEvict
,@Caching
的使用 - 深入理解 data-redis-cache 的实现原理
文章内容说明
- 如何使用 redis-cache
- 自定义 keyGenerator 和过期时间
- 源码解读
- 自带缓存机制的不足
快速入门
maven 加入 jar 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 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<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
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<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 的默认配置
最后看一下,它的切点是如何定义的,即何时会调用 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-maven
spring-data-redis-cache 使用及源码走读的更多相关文章
- Spring Data Redis—Pub/Sub(附Web项目源码)
一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...
- Spring Data Redis—Pub/Sub(附Web项目源码) (转)
一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...
- Redis与Spring Data Redis
1.Redis概述 1.1介绍 官网:https://redis.io/ Redis是一个开源的使用ANSIC语言编写.支持网络.可基于内存 亦可持久化的日志型.Key-Value型的高性能数据库. ...
- spring mvc Spring Data Redis RedisTemplate [转]
http://maven.springframework.org/release/org/springframework/data/spring-data-redis/(spring-data包下载) ...
- Spring Data Redis简介以及项目Demo,RedisTemplate和 Serializer详解
一.概念简介: Redis: Redis是一款开源的Key-Value数据库,运行在内存中,由ANSI C编写,详细的信息在Redis官网上面有,因为我自己通过google等各种渠道去学习Redis, ...
- Spring Data Redis 2.x 中 RedisConfiguration 类的新编写方法
在 Spring Data Redis 1.x 的时候,我们可能会在项目中编写这样一个RedisConfig类: @Configuration @EnableCaching public class ...
- Spring Data Redis入门示例:字符串操作(六)
Spring Data Redis对字符串的操作,封装在了ValueOperations和BoundValueOperations中,在集成好了SPD之后,在需要的地方引入: // 注入模板操作实例 ...
- 使用Spring Data Redis时,遇到的几个问题
需求: 1,保存一个key-value形式的结构到redis 2,把一个对象保存成hash形式的结构到redis 代码如下: // 保存key-value值 pushFrequency ...
- Spring Data Redis 让 NoSQL 快如闪电 (1)
[编者按]本文作者为 Xinyu Liu,详细介绍了 Redis 的特性,并辅之以丰富的用例.在本文的第一部分,将重点概述 Redis 的方方面面.文章系国内 ITOM 管理平台 OneAPM 编译呈 ...
- Spring Data Redis示例
说明 关于Redis:一个基于键值对存储的NoSQL内存数据库,可存储复杂的数据结构,如List, Set, Hashes. 关于Spring Data Redis:简称SDR, 能让Spring应用 ...
随机推荐
- zoj - 4059 Kawa Exam scc + dsu
Kawa Exam 题解:先scc把图变成树, 然后对于这若干棵树在进行dsu的操作. dsu就是先找到最大的子树放在一边,然后先处理小的子树,最后处理大的子树.无限递归. 重要的一点就是 是否重新添 ...
- poj 1797Heavy Transportation(dijkstra变形)
题目链接:http://poj.org/problem?id=1797 题意:有n个城市,m条道路,在每条道路上有一个承载量,现在要求从1到n城市最大承载量,而最大承载量就是从城市1到城市n所有通路上 ...
- 工作中遇到的99%SQL优化,这里都能给你解决方案(三)
-- 示例表 CREATE TABLE `employees` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL ...
- java读写文件IO
package Common.readFile; import Common.tool.User; import com.fasterxml.jackson.databind.ObjectMapper ...
- Android的消息循环与Handler机制理解
一.概念 1.事件驱动型 什么是事件驱动?就是有事了才去处理,没事就躺着不动.假如把用户点击按钮,滑动页面等这些都看作事件,事件产生后程序就执行相应的处理方法,就是属于事件驱动型. 2.消息循环 把需 ...
- group by语法
group by 用法解析 group by语法可以根据给定数据列的每个成员对查询结果进行分组统计,最终得到一个分组汇总表. SELECT子句中的列名必须为分组列或列函数.列函数对于GROUP BY子 ...
- Python集训营45天—Day03
目录 1. 分支结构 1.1 初步介绍 1.2 使用案例 1.3 练习 2.循环结构 1.1 初步介绍 1.2 使用案例 1. 分支结构 1.1 初步介绍 至今,我们所写的Python代码都是顺序执行 ...
- FaceBook快捷登入
关于集成FaceBook快捷登入,我上回做了个最简单的版本,所有Web端通用,在这边共享下,有更好的解决方案的,麻烦评论留个地址,有不妥之处请指正. 首先,我们先加载Facebook的Js windo ...
- CentOS7 常用命令
常用命令 1.文件与目录操作 命令 解析 cd /home 进入 ‘/home’ 目录 cd .. 返回上一级目录 cd ../.. 返回上两级目录 cd - 返回上次所在目录 cp file1 fi ...
- NPOI导出数值格式设置(我是保留四位小数,不足补0)
看了网上好多帖子,都是保留两位小数的,写法是: HSSFDataFormat.GetBuiltinFormat("0.00"); 于是想四位小数,就是多加两个00,变成: HSSF ...