预期读者

  • 准备使用 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. POJ 3070 Fibonacci 矩阵快速幂模板

    Fibonacci Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 18607   Accepted: 12920 Descr ...

  2. CF991D Bishwock 第十七 贪心

    Bishwock time limit per test 1 second memory limit per test 256 megabytes input standard input outpu ...

  3. ZOJ 3870 Team Formation 位运算 位异或用与运算做的

    For an upcoming programming contest, Edward, the headmaster of Marjar University, is forming a two-m ...

  4. 自荐RedisViewer一个有情怀的跨平台Redis可视化客户端工具

    自荐一个有情怀的跨平台Redis可视化客户端工具--RedisViewer 转载自 最美分享Coder 2019-09-17 06:31:00 介绍 在以往的文章中曾经介绍过几款Redis的可视化工具 ...

  5. TCP/IP协议,TCP与平台通信,通讯协议压力测试(python)

    最近的项目来了一个需求,要求测试tcp网关通讯协议: 1.液压井盖通过TCP/IP TCP与平台通信: 2.硬件定期发送心跳包(10S)给平台,是平台与硬件保持长连接: 3.每台硬件有一个12字节的唯 ...

  6. 从零开始构建Linux

    目的:深入了解以Linux内核为基础的系统是如何组成,运行,以构建一个最基础的,纯净的系统. LFS构建步骤宿主机准备- linux操作系统安装- 使用独立硬盘,创建分区- 配置用户和组- 下载所有需 ...

  7. 从零开始入门 K8s| 详解 Pod 及容器设计模式

    作者|张磊 阿里云容器平台高级技术专家,CNCF 官方大使 一.为什么需要 Pod 容器的基本概念 我们知道 Pod 是 Kubernetes 项目里面一个非常重要的概念,也是非常重要的一个原子调度单 ...

  8. spring中ehcache的配置和使用方法

    继续上篇,这篇介绍服务层缓存,ehcache一般的配置和用法 一.添加jar包引用 修改pom.xml文件,加入: <dependency> <groupId>org.spri ...

  9. 提交任务到spark(以wordcount为例)

    1.首先需要搭建好hadoop+spark环境,并保证服务正常.本文以wordcount为例. 2.创建源文件,即输入源.hello.txt文件,内容如下: tom jerry henry jim s ...

  10. Go微服务容错与韧性(Service Resilience)

    Service Resilience是指当服务的的运行环境出现了问题,例如网络故障或服务过载或某些微服务宕机的情况下,程序仍能够提供部分或大部分服务,这时我们就说服务的韧性很强.它是微服务中很重要的一 ...