缓存的框架太多了,各有各的优势,比如Redis、Memcached、Guava、Caffeine等等。
如果我们的程序想要使用缓存,就要与这些框架耦合。聪明的架构师已经在利用接口来降低耦合了,利用面向对象的抽象和多态的特性,做到业务代码与具体的框架分离。
但我们仍然需要显式地在代码中去调用与缓存有关的接口和方法,在合适的时候插入数据到缓存里,在合适的时候从缓存中读取数据。
想一想AOP的适用场景,这不就是天生就应该AOP去做的吗?
是的,Spring Cache就是一个这个框架。它利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。而且Spring Cache也提供了很多默认的配置,用户可以3秒钟就使用上一个很不错的缓存功能。
一、什么是Spring Boot Cache?
Spring Cache本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用Spring Cache还需要具体的缓存实现来完成。
Spring Boot 集成了多种cache的实现,如果你没有在配置类中声明CacheManager或者CacheResolvoer,那么SpringBoot会按顺序在下面的实现类中寻找:
-
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
-
使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
二、简单使用SpringCache
分为很简单的三步:加依赖,开启缓存,加缓存注解。
1) 加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2 )开启缓存
在启动类加上@EnableCaching
注解即可开启使用缓存。
@SpringBootApplication
@EnableCaching
public class CachingApplication {
public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}
}
3)加缓存注解
在要缓存的方法上面添加@Cacheable
注解,即可缓存这个方法的返回值。
@Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
public List<NewJob> queryAll(User uid) {
return newJobDao.findAllByUid(uid);
}
此处的value
是必需的,它指定了你的缓存存放在哪块命名空间。
此处的key
是使用的spEL表达式,参考上章。这里有一个小坑,如果你把methodName
换成method
运行会报错,观察它们的返回类型,原因在于methodName
是String
而methoh
是Method
。
此处的User
实体类一定要实现序列化public class User implements Serializable
,否则会报java.io.NotSerializableException
异常。
需要注意的是,调用加了@Cacheable 的方法,调用方法必须跟加了@Cacheable 的方法 在不同的类中,跟反向代理有关,不然不会生效。
三、SpringCache中的概念以及用法
1) 相关概念:
名称 | 解释 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 与@Cacheable区别在于是否每次都调用方法,常用于更新 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
CacheManager, Cache是接口
@Cacheable/@CachePut/@CacheEvict 是放在方法尚主要控制缓存的注解
@EnableCaching 放在启动类上
@CacheConfig 放在配置类上。
2)@Cacheable/@CachePut/@CacheEvict 主要的参数
名称 | 解释 |
---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写, 如果不指定,则缺省按照方法的所有参数进行组合 例如: @Cacheable(value=”testcache”,key=”#id”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false, 只有为 true 才进行缓存/清除缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
unless | 否定缓存。当条件结果为TRUE时,就不会缓存。 @Cacheable(value=”testcache”,unless=”#userName.length()>2”) |
allEntries (@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true, 则方法调用后将立即清空所有缓存 例如: @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation (@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true, 则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法 执行抛出异常,则不会清空缓存 例如: @CachEvict(value=”testcache”,beforeInvocation=true) |
例如:
@Cacheable(value = "DEMO:TEST" ,key = "#id")
public Demo getDemo(Long id){
System.out.println("序号"+id);
return new Demo("1234455", LocalDateTime.now());
}
当第一次调用时会进入方法,打印id:
第二次调用时就不会调用方法,redis中就会多出一个key:
我们生成的key就是 vaule + key中设置的值。但是此处目前有一个坑,就是生成的key在 value 与 key的衔接处是双引号。这个后面可以通过配置修改为单引号。
3) Spel表达式
一、spel语法
具体语法可以参考此篇博客:SpEL表达式总结 - 简书
二、SpringCache也提供了root对象,具体功能使用如下。
4) 使用spel表达式栗子
1、使用参数作为key:使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。
@Cacheable(value="users", key="#id")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#p0")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#user.id")
public User find(User user) {
returnnull;
}
@Cacheable(value="users", key="#p0.id")
public User find(User user) {
returnnull;
}
2、当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
returnnull;
}
如果要调用类里面的方法:
@Cacheable(value={"TeacherAnalysis_public_chart"}, key="#root.target.getDictTableName() + '_' + #root.target.getFieldName()")
public List<Map<String, Object>> getChartList(Map<String, Object> paramMap) {
}
public String getDictTableName(){
return "";
}
public String getFieldName(){
return "";
}
3、最好使用所有参数作为key,当然,也分情况。
@Cacheable(cacheNames = "c2",key = "#id")
public User getUserById(Long id,String username){
User user = new User();
user.setId(id);
return user;
}
@Test
void testGetUserById() {
User u1 = userService.getUserById(98L, "dong");
User u2 = userService.getUserById(98L, "lisi");
}
以参数id作为key会出现逻辑错误,当调用第一次getUserById方法时,存入key为id,值为dong,当调用第二次getUserById方法时,因为已经存入缓存id,所以不会进入第二次getUserById方法,所以lisi不能进入缓存
5) 自定义key生成器
@Component
public class MyKeyGenerate implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + ":"
+ method.getName() + ":"
+ StringUtils.arrayToDelimitedString(params, ":");
}
}
//将myKeyGenerate注入
@Cacheable(cacheNames = "test",keyGenerator = "myKeyGenerate")
public User getUserById(Long id,String username){
User user = new User();
user.setId(id);
user.setUsername(username);
return user;
}
这个要讲的不是很多,自定义key生成器内容比较简单,有兴趣可以自行再搜索一下。
6)condition
符合条件的情况下才缓存。方法返回的数据要不要缓存,可以做一个动态判断。
7)unless
否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。
一般用于结果不为空时判断 unless = "#result.data==null"
四、各个注解详解
在上面,已经把各个注解的公共属性抽了出来,这里只做一些注解的特有属性,当然,可能某些属性也是公有的。
1)@Cacheable:
在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。
1、unless:条件符合则不缓存,是对出参进行判断
unless属性可以使用#result表达式。效果: 缓存如果有符合要求的缓存数据则直接返回,没有则去数据库查数据,查到了就返回并且存在缓存一份,没查到就不存缓存。
condition 不指定相当于 true,unless 不指定相当于 false
当 condition = false,一定不会缓存;
当 condition = true,且 unless = true,不缓存;
当 condition = true,且 unless = false,缓存;
2、sync:是否使用异步,默认是false.
在一个多线程的环境中,某些操作可能被相同的参数并发地调用,同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待。当值为true,相当于同步可以有效的避免缓存击穿的问题。
@Cacheable(value="user_cache",key="#userId", unless="#result == null")
public User getUserById(Long userId) {
User user = userMapper.getUserById(userId);
return user;
}
2)@CachePut
@CachePut
注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable
不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。但需要注意的是该注解的value
和 key
必须与要更新的缓存相同,也就是与@Cacheable
相同。示例:
@CachePut(value = "emp", key = "targetClass + #p0")
public NewJob updata(NewJob job) {
NewJob newJob = newJobDao.findAllById(job.getId());
newJob.updata(job);
return job;
}
@Cacheable(value = "emp", key = "targetClass +#p0")//清空缓存
public NewJob save(NewJob job) {
newJobDao.save(job);
return job;
}
也就是说@Cacheable注解 是在调用方法之前去看看是否已经有了缓存 如果有缓存就不会执行方法,@CachePut注解 是不管有没有缓存 都先执行方法,然后将方法的结果更新缓存。
3) @CacheEvict:清空缓存
注解的方法在调用时会从缓存中移除已存储的数据。
@CacheEvict(value = "user_cache", key = "#id")
public void deleteUserById(Long id) {
userMapper.deleteUserById(id);
}
1、allEntries:是否清空左右缓存。默认为false
当指定了allEntries为true时,Spring Cache将忽略指定的key
2、beforeInvocation:是否在方法执行前就清空,默认为 false
清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
4)@Caching:
可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解
1、其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
@Caching(
cacheable = {@Cacheable(value = "stu",key = "#userName")},
put = {@CachePut(value = "stu", key = "#result.id"),
@CachePut(value = "stu", key = "#result.age")
}
)
public Student getStuByStr(String userName) {
StudentExample studentExample = new StudentExample();
studentExample.createCriteria().andUserNameEqualTo(userName);
List<Student> students = studentMapper.selectByExample(studentExample);
return Optional.ofNullable(students).orElse(null).get(0);
}
5)配置@CacheConfig#
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"myCache"})
注解来统一指定value
的值,这时可省略value
,如果你在你的方法依旧写上了value
,那么依然以方法的value
值为准。
使用方法如下:
@CacheConfig(cacheNames = {"myCache"})
public class BotRelationServiceImpl implements BotRelationService {
@Override
@Cacheable(key = "targetClass + methodName +#p0")//此处没写value
public List<BotRelation> findAllLimit(int num) {
return botRelationRepository.findAllLimit(num);
}
.....
}
五、自定义过期时间
关于自定义配置如果需要理解,需要简单得了解一下源码,这里就不做详细讲解了,大概说一下如何。
首先要实现 RedisCacheManager 接口,创建实现类。自定义RedisCache。如下所示:
public class PlusCacheManager extends RedisCacheManager {
public PlusCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
if (array.length > 1) {
long ttl = Long.parseLong(array[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}
大概讲解一下,这个重写得 createRedisCache 方法,就是自定义实现 RedisCache 的方法, name 就是 @Cacheable 中 value的值。所以这个方法可以起到过虑 name 作用
例如: @Cacheable(value = "name#3600" ,keyGenerator = "myKeyGenerate")
这个value 值是:name#3600 , 上面的方法可以将name 用 ’#‘ 分割,如果 '#' 后面有值,就可以将 ’#‘ 后面的值设置进入缓存时间中。
另外还需要增加一个配置类,用于自定义RedisCacheManager,如下所示,就可以简单的解决自定义key的时间问题。还有key键中的双引号问题。
@Bean
@Primary
public PlusCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 60s缓存失效
.entryTtl(Duration.ofSeconds(60))
// 设置key的序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
// 设置value的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.computePrefixWith((name) -> RedisKeyConstants.CACHE + name + StrUtil.COLON)
// 不缓存null值
.disableCachingNullValues();
PlusCacheManager plusCacheManager = new PlusCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), config);
log.info("自定义RedisCacheManager加载完成");
return plusCacheManager;
}
// key键序列化方式
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
// value值序列化方式
private GenericJackson2JsonRedisSerializer valueSerializer(){
ObjectMapper objectMapper = new ObjectMapper();
// 反序列化时候遇到不匹配的属性并不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 序列化时候遇到空对象不抛出异常
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 反序列化的时候如果是无效子类型,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
// 不使用默认的dateTime进行序列化,
objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// 使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类
objectMapper.registerModule(new JavaTimeModule());
// 启用反序列化所需的类型信息,在属性中添加@class
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 配置null值的序列化器
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}