1 springCache是什么?
2 怎么用?
3 基本原理。
- 在了解springCache 是什么的的前提我们要补下什么是内存,外存
内存(Memory)也被称为内存储器,其作用是用于暂时存放 CPU 中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到内存中进行运算,当运算完成后 CPU 再将结果传送出来,内存的运行也决定了计算机的稳定运行,此类储存器一般断电后数据就会被清空。
外部存储器是除了计算机内存 及cpu 缓存以外的数据,断电后还能保存数据,其实就是像硬盘、软盘、光盘、U 盘等,一般的软件都是安装在外存中(windows系统指的是== CDEF 盘==, Linux 系统指的是挂载点)
缓存就是把一些外存上的数据保存到内存上而已,如果要想使一个值放到内存上,实质就是在获得这个变量之后,用一个生存期较长的变量存放你想存放的值,在 java中一些缓存一般都是通过 map 集合来做的。
- 广义的缓存是把一些慢存(较慢的外存)上的数据保存到快存(较快的存储)上
- 简单讲就是,如果某些资源或者数据会被频繁的使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都从数据库或者硬盘上去获取,速度会很慢,会造成性能问题(系统停工待料)。于是我们把这些数据冗余一份到快存里面,每次操作的时候,先到快存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取它,并复制一份到快存中,下一次访问的时候就可以直接从快存中获取。从而节省大量的时间,可以看出,缓存是一种典型的空间换时间的方案。
- 缓存命中率、
命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
Miss 率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数]) - 移除策略
您可能问为什么要移除缓存,==根据二八原则,一个系统20%的业务数据是往往对应着外部80%的请求,所以我们的缓存往往只缓存20%==哪些重要的数据。 所以引出一个问题该如何设置缓存来保存那20%的数据呢。这里简单介绍几个缓存的策略:
FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除;
LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除;
LFU(Least Frequently Used):最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除。
TTL(Time To Live ):存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)
TTI(Time To Idle):空闲期,即一个数据多久没被访问将从缓存中移除的时间。
实际设计缓存时以上重要指标都应该考虑进去,当然根据实际需求可能有的指标并不会采用进设计去
例子: 在我的工作中,平台对外提供大量的接口调用,Qps 达到100次每秒,当平台接收到第三方调用我们接口时,我们会先校验下,对方是否为我们公司的后台已经注册具有调用权限的,没有权限的第三方将被拒绝,而这个权限 我们是保存在redis 中,因为每一次的接口调用,都要验证权限,所以保存上百家第三方在我们平台注册的权限的数据(而且权限数据真的很少,但是每次还都要必须访问,所以直接写到redis中就完了。),所以并没有设置移除策略。
使用方式
一般用在服务层,
即service 实现类 中 注入一个map,或者redis 来缓存从dao 层中查询出的数据。
通常做法就是这个样子:
未用springCache前:
public Provinces detail(String provinceName) {
Provinces provinces = null;
//从redis 中根据 provinceName 为key 查redis 中的value
provinces = (Provinces)redisTemplate.opsForValue().get(provinceName);
//如果每次从缓存中取出来后,就重新刷新一遍时间。
if(null != provinces){
redisTemplate.expire(provinceName,30,TimeUnit.SECONDS);
System.out.println("缓存中得到数据");
//直接返回
return provinces;
}
//查询数据库
provinces = super.detail(provinceName);
if (null != provinces){
//查询出的数据要放入redis 缓存中
redisTemplate.opsForValue().set(provinceName,provinces,30,TimeUnit.SECONDS);
}
return provinces;
}
** 从上面的代码可以看到,其实最主要的代码只有 provinces = super.detail(provinceName); 代表从数据库查询,这是我们的业务必须的业务代码,而从缓存中取出数据和把数据库查询的结果存储到redis 中,有时候这并不是我们的业务必须的,这是我们为了提高系统性能做的手段!!!**所以您想到了 使用Spring AOP 来横切我们的 这个业务代码,把上面那些代码就简化成:
使用sprigCahe 后
@Cacheable
public Provinces detail(String provinceName) {
return provincesDao.detail(provinceName);
}
看起来很爽吧,仅仅用一个@Cacheable 代替一大扒拉的代码
所以不必担心 spring 为我们实现了 这个东东就是 springCache ,提供几个注解 @Cacheable,@CachePut , @CacheEvict
而spring 为我们提供的仅仅是个规范,因为我们使用哪个缓存器,用mongoDB ,redis memchcahe,…等等来缓存都是有能的,那么spring 定义的是一个规范,它仅仅是把 一个流程抽象出来(注意流程是:在查询前去查缓存,查出后把数据放入缓存,更新数据时,在数据库更新后,在更新缓存里的数据,在请求删除数据时,删除数据后再删除缓存数据,这是一个通用的流程)。
使用SpringCahe配置:
依赖:
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-cache</artifactId>-->
<!--</dependency>-->
配置文件
此处以springBoot 配置 redis 为例:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200111182917598.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTk1NTMyNw==,size_16,color_FFFFFF,t_70)
第一步创建一个配置类:
@Configuration
@EnableCaching // 启用缓存配置 使@Cacheable,@CachePut , @CacheEvict 等注解生效。
public class CacheConfig extends CachingConfigurerSupport{
//
//key的生成,springcache的内容,跟具体实现缓存器无关
//自定义本项目内的key的方式
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
// sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
//不支持过期时间
// @Bean
// public CacheManager cacheManager() {
// //jdk里,内存管理器
// SimpleCacheManager cacheManager = new SimpleCacheManager();
// cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province")));
// return cacheManager;
// }
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(320))) //缓存时间绝对过期时间20s
.transactionAware()
.build();
}
}
然后在服务层的实现类上加上注释,和对应的方法上加上注释:
@Service("provincesService")
@CacheConfig(cacheNames = "province") //通用配置
public class ProvincesServiceImpl2 extends ProvincesServiceImpl implements ProvincesService {
@Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个
public Provinces detail(String provinceid) {//一个接口方法,对应一个缓存器
return super.detail(provinceid);
}
//这个AOP先改库,再删除缓存
// @CachePut(key = "#entity.provinceid") //发现被缓存,并且缓存的名字是province::参数值
@CachePut(key = "#root.targetClass.simpleName+#entity.provinceid.toString()")
public Provinces update(Provinces entity) {
return super.update(entity);
}
@CacheEvict(cacheNames = "province")
public void delete(String provinceid) {
super.delete(provinceid);
}
//组合配置
@Caching(put = {
@CachePut(key = "#entity.provinceid"),
@CachePut(key = "#entity.provinceid")}
)
@CachePut
public Provinces add(Provinces entity) {
return super.add(entity);
}
}
总结 配置:
- pom依赖
- 配置类
继承了CachingConfigurerSupport 重写了 keyGenerator
配置一个CanchManager 的Bean - 在具体服务实现类上配置 @CacheConfig(cacheName=“xxx”) 对应的具体查询方法使用 @Cacheable 对应更新和新增用:@CachePut ,对应的删除 @CacheEvict
配置原理
解释配置类
其实springCache,实现的基本原理通过我们上面的代码没使用注解前和只用注解后来看前后对比,根据在查询数据库前查,做缓存,查询数据库后存入缓存的流程,您肯定知道了这是典型的 环绕AOP 增强来实现的。
这就引出了springCache 作者需要解决的问题,进行增强的的代码就是查缓存,可是查缓存得有key吧,数据的存储是使用key-value 形式,那么key 怎么取?
从最开始我们没有用注解的代码中看到,key 我们直接使用的是入参,但是往往业务中,有时不仅仅是一个简单的类型,可能入参是一个对象,而业务又必须以这个对象中的一个属性来作为key 查询怎么办?
那么我们需要了解注解的具体使用方法:
@Cachable:
参数 | 解释 | 举例 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @Cacheable(value=”mycache”),@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 如果所标注的方法入参是 String id 则: #id ,如果入参是对象 User user ,并且我们的业务使用对象的id 属性来查询 则:@Cacheable((value=”testcache”,key="#user.id" |
condition | 缓存的条件,因为有时需要细化数据库查询出来的数据做个判断才能决定要不要缓存,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存, | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
@CachePut:
参数 | 解释 | 举例 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CachePut(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CachePut(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
特别说明: 原来我以为,这个是为了那些,已经在缓存中的数据,在真实的数据需要更新时为了避免其他请求读到过时的数据,保持数据库和缓存的一直性来使用的,但其实不是,不论缓存中是否已经存有要更新的数据,在执行完@CachePut标注的方法后,这个方法的返回值都会把结果缓存到缓存器中(redis,MongoDB,memcache)
@CacheEvict
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CacheEvict(value=”my cache”) |
---|---|---|
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CacheEvict(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
@CacheConfig
英文这样的解释
@CacheConfig is a class-level annotation that allows to share the cache names
意思用来配置通用配置!标注在类上!
那么上面的三个配置 @Cacheable @CachePut @CacheEvict 在举例的过程中都有 value =“xxx”
那么配置 在类上:
实例:
您可能会疑惑为啥要配置 cacheName ,因为:springCache对缓存的使用是通过分组使用,想要取value 值您不仅需要提供key ,还需要提供一个cacheName 相当于组名:程序真正去库中查询数据时不仅用的是key 还有组名,他会将组名+"::" 拼接成为key 的前缀,不要问我问什么,因为我看到了:
@CacheConfig(cacheNames = "province") //通用配置
public class ProvincesServiceImpl2 extends ProvincesServiceImpl implements ProvincesService
那么在方法上就不必在 写 value,这里你会疑惑 代替的是@Cacheable @CachePut @CacheEvict 中的value 属性,怎么用的是@CacheConfig的 cacheName 属性来代替,而不是用value 属性来替代,那是因为@Cacheable@CachePut @CacheEvict 源码给这个value 属性起了一个别名:有源码图为证:
特别说明,如果@Cacheable @CachePut @CacheEvict 在使用的时候直接标注没有任何的使用属性值,会默认使用方法的入参值作为,key, 但是有一个唯一的情况例外,就是我们配置了 keyGenerator 这个key 的生成器。这个生成器,对@Cacheable @CachePut @CacheEvict 都是生效的。
所有又引出了一个疑问,
为啥子要有这个生成器keyGenerator?
因为我们在存key的时候不只是简简单单的存id,可能还需要+ 业务名称比如,例如当强类的名字字符串拼接上去,这样更加规范些。
将上面的配置代码拿出部分解释下:
return new KeyGenerator() {
//这里的tartget 是被增强的方法,mehtod是被增强的方法,parapms被增强方法的入参
//所以这里的生成规范就是 当前被增强的类名和参数值
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
// sb.append(method.getName()); //如果不注释会造成@CachePut 会重复复存储相同的value,因为方法名字不同,导致key 不同。
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
特别注意:key 最终的值是: @CacheConfig(cachename=“ddd”) 中的 “ddd” +"::"+target.getClass().getSimpleName()+所有参数值字符串。(不要问题为啥最终储存在redis 中的key 前缀为啥是ddd:: 因为他就这么存的。)
被aop增强的方法,使用的key 都来自这里生成,但是如果@Cacheable @CachePut @CacheEvict 都有设置属性值key 如:@Cacheable(key=“xx”) @CachePut(key=“xx”) @CacheEvict(key=“xx”) 那么aop增强时查询用的key 是用注解的属性值优先,而不是keyGenerator 生成器生成的值。
CacheManager 配置目的
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(20))) //缓存时间绝对过期时间20s
.transactionAware() //事务
.build();
目的就是解决 我们用那个缓存器,这里是以整合配置redis 为例,所以当然也可以使用jdk自身支持的缓存器:
//不支持过期时间
@Bean
public CacheManager cacheManager() {
//jdk里,内存管理器
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province")));
return cacheManager;
}
redis 配置原理
这里介绍 配置redis 的原理:
-
cacheManager 方法的入参 在springBoot 环境中会会把配置文件 application.yml 中的redis 的地址 密码端口封装到入参的RedisConnectionFactory 中
-
首先 我们要知道redis 这个中间件它在存储对象时是不提供序列化机制的,redis 官方把使用序列化的选择权交给了我们开发者自由配置!!所以要配置 key 的序列化, value 序列化。
我们观察源码发现:
根据提示: key 使用的是 StringRedisSerializer 来序列化
value 使用的是JdkSerializationRedisSerializer 来序列化
看到这里,如果您使用springBoot 整合过redis用RedisTemplate,立刻就熟悉了:帮您回忆一下我们如果整合redisTemplate时配置类是:
@Bean
@Resource //application.yml 文件中的 redis的配置会被 RedisConnectionFactory 封装
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
//把连接工厂给模板
template.setConnectionFactory(factory);
//redis 存储时需要把对象序列化,序列化的方式,自己配置,解耦选择一个自己喜欢的方式
//下面使用jdk序列化
template.setValueSerializer(new JdkSerializationRedisSerializer());
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
这里也用了 JdkSerializationRedisSerializer StringRedisSerializer
终于写完了…
我们总结下:
使用 springCache
1 pom
2 配置类
1)keyGenerator
2) CacheManager
3 @CacheConfig 对类的通用标注
@Cacaheable @CachePut @CacheEnict 对方法标注
同时注意注解中属性的含义!!
最后郑重说明:文章内容由涉及到享学例子和其他网友的介绍用自己的实践写的文章,如果觉的侵权可留言删除。谢谢!