文章目录
- Spring Cache 的使用
- 注解形式的使用
- 在同一个类调用加了缓存的方法失效的原因
- api 接口形式的使用
在写程序的时候,对于频繁访问的数据,我们一般会使用缓存,将数据存储在内存中,方便下次直接在内存中读取,而不需要再去查询数据库或者再进行复杂运算得到。
Spring Cache 的使用
首先介绍一下Spring提供的Cache接口,并且提供了默认的实现 ConcurrentMapCache
,看类名就知道,使用的ConcurrentMap实现。这里主要介绍一下注解形式。(另:个人更推荐接口的方式使用缓存,注解使用起来虽然方便,但是控制粒度不够,不能随时随地使用。当然需求简单的话注解就足够了)
注解形式的使用
首先看一下spring cache提供的注解,其实看名字就知道做什么了。这里主要简单介绍几个。
-
EnableCaching
开启缓存
使用注解式缓存首先得在启动类或缓存配置类上添加该注解,才能启动缓存 Cacheable
使用缓存
在方法上使用(或在类上,所有公用方法的返回都会被缓存)。当存在缓存时,直接从缓存中读取值并返回;不存在则调用方法,并将结果存入缓存中。该注解有以下常用属性:
- value等同于cacheNames用于指定缓存名称;
- key 指定缓存的key,可以使用spel表达式构建
- keyGenerator(需要自己配置对应的bean)指定key的生成策略根据参数生成key,和key互斥
- condition和unless都是指定条件下进行缓存,但unless是在返回之后判断是否进行缓存
- cacheManager 指定自定义的缓存管理器(可以详细配置缓存的各种属性,过期时间,序列化方式什么的),都偷懒用注解形式了,一般也不会有什么特别要求吧
-
sync
是否同步,只允许同时一个线程对某个key进行读写,默认false
@Cacheable(cacheNames = "studentCache", key = "#args[0]+'_'+#args[1]", unless = "#result == null")
public Student getBySnoAndName(String sno, String name) {
// 指定缓存名为 studentCache,key为 sno_name, 当返回为null时不缓存
}
-
CachePut
存缓入存
同样在方法和类上使用,该注解只将方法的返回值存入缓存中,调用方法时不回从缓存中读取值。相关属性和 Cacheable 差不多,少一个 sync. 使用示例
@CachePut(cacheNames = "studentCache", key = "#student.sno+'_' +#student.name", condition = "#student!=null")
public Student addStudent(Student student) {
// 将该方法的结果放入 studentCache 中, 条件 student != null
}
CacheEvict
使缓存失效
作用域同方法和类。相关属性和上面的差不多,但还有另外两个
- allEntries 是否所有的缓存的值失效,默认false
- beforeInvocation 是否在调用前执行, 默认false,主要考虑存在方法报错的情况,false的话,如果方法报错,就不会执行。
@CacheEvict(cacheNames = "studentCache", key = "#sno +'_'+#name", beforeInvocation = true)
public boolean deleteById(String sno, String name) {
// 执行该方法前将对应的缓存清除掉, 其实到这里大概就会发现注解式缓存的缺点了
// 看方法名就知道,这个方法本来是根据id删除的,为了举例临时改了一下
// 因为key是 sno_name (实际上sno作为key就够了,或者id也可以),但传入的参数为id,根本获取不到key。这就是注解的局限性
}
在同一个类调用加了缓存的方法失效的原因
注意 在使用注解的时候,在同一个类内部调用是会失效的!!!因为spring cacle注解使用aop实现的,实际上调用的是动态代理的方法。因为调用内部方法是有this的, 在生成代理类后,this就指向原对象了,而不会调用生成的代理对象的方法。
// 在内部调用方法,实际上是有this的,只是一般会省略。在生成代理类是 调用的还是原方法,而不是加了缓存的代理的方法
public Student getByStudent(Student stu) {
return this.getBySnoAndName(stu.getSno(), stu.getName());
}
@Cacheable
public Student getBySnoAndName(String sno, String name) {
// todo
}
-
Caching
将上述几个注解组合使用
直接看源码吧,没啥讲的,就三个数组。
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
// 使用
@Caching(cacheable = {@Cacheable(cacheNames = "stuCache1"),@Cacheable(cacheNames = "stuCache2")})
api 接口形式的使用
其实网上一搜 Spring cache 使用,大部分介绍注解方式的。所以我这里再介绍一下api接口形式。个人也更偏向于使用这种形式,功能性更强。直接上代码吧。
首先是定义一个 CacheManager 的bean,上面有提到。这里以RedisCacheManager 为例
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 缓存的配置信息
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHour(2));
// 初始化的缓存, 可以指定相关的配置
Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(4);
configurationMap.put("studentCache", redisCacheConfiguration);
RedisCacheManager cacheManager = RedisCacheManager.builder(redisCacheWriter)
.withInitialCacheConfigurations(configurationMap) // 加入初始化的自定义缓存配置
.cacheDefaults(redisCacheConfiguration()) // 设置的默认配置,就都用同一个了
.transactionAware()
.build();
return cacheManager;
}
然后使用缓存的时候需要使用redisCacheManager获取缓存对象
@Autowired
private RedisCacheManager redisCacheManager;
private Cache studentCache;
/**
* 在初始化后调用该方法, 获取 studentCache 缓存
* 注: Bean 初始化顺序, 构造方法 -> @Autowired -> @PostConstruct
*/
@PostConstruct
public void init() {
studentCache = redisCacheManager.getCache("studentCache");
}
这样就可以直接在代码中使用缓存了。可以先看一下Spring Cache 接口提供的方法
-
ValueWrapper get(Object key);
根据key获取封装过的value,可以区分缓存中key是否存在
public StudentInfo getBySno(String sno) {
Student s;
Cache.ValueWrapper wrapper = studentCache.get(sno);
if (wrapper != null) { // wrapper 为null,说明缓存中不存在
s = (Student) wrapper.get(); // get也可能返回null,说明缓存中存了一个null
} else {
s = getBySnoFromDB(sno); // 缓存不存在查数据库
studentCache.put(sno, s); // s 如果是null,上面wrapper就会get null
}
return s;
}
-
<T> T get(Object key, @Nullable Class<T> type);
返回指定类型的value,缓存不存在返回null
public StudentInfo getBySno(String sno) {
Student s = studentCache.get(sno, Student.class);
if (s == null) { // 这个方法不回区分缓存不存在还是存入了null, 两种情况都是null
s = getBySnoFromDB(sno);
studentCache.put(sno, s);
}
return s;
}
-
<T> T get(Object key, Callable<T> valueLoader);
返会指定类型的value,如果缓存不存在,通过valueLoader获取,并存入缓存
public StudentInfo getBySno(String sno) {
return studentCache.get(sno, sno -> getBySnoFromDB(sno));
}
-
void put(Object key, @Nullable Object value);
存入key-value缓存 -
ValueWrapper putIfAbsent(Object key, @Nullable Object value);
缓存不存在的时候存入,并返回null或之前的值 -
void evict(Object key);
清除指定key的缓存 -
void clear();
清空所有的key