写在前面
本文参考自 Spring Boot 官方文档
简介
对于缓存,我们常说的应该是 redis
来做缓存了,但是 Spring
难道仅仅只支持特定的 redis
来做缓存嘛?Spring 经常强调的思想是:”俺们不重复造轮子,俺们只是轮子的适配者>“。
Spring 框架支持向应用程序透明地添加缓存。你可以自由地选择缓存的具体实现。抽象的核心是将缓存应用于方法,从而减少了基于缓存中可用信息的执行次数。缓存逻辑对于应用程序来说是透明的,对调用程序没有干扰。只要通过 @EnableCaching
注释启用了缓存支持,Spring Boot 就会自动配置缓存的基础设施。
缓存的抽象并没有提供实际的存储,抽象的好处就在于我们不用依赖某种具体实现,我们可以在生产期间更换不同的实现,对代码却不会产生什么影响(除非你觉得更换是有必要的,否则仍然不建议这么做)。缓存的抽象主要依赖于 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager
接口。
如果没有添加任何特定的缓存库,Spring Boot 将自动配置一个简单的 ConcurrentMap
实现的缓存。
支持的缓存实现库
如果没有定义 CacheManager
类型的 bean 或 名为 cachedResolver
的 CacheResolver
, Spring Boot 将按以下的顺序检测提供程序:
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
我们仍然可以选择通过 spring.cache.type
来强制使用特定的缓存提供程序。或者 spring.cache.type = none
来禁用缓存。有关以上特定的缓存库介绍,可以查看文档(指导了你需要确保怎样的jar
包需要在你的类路径下)。
简单使用
抽象的好处开始体现,想要我们针对不同的缓存库写代码??
所以,我们只需要 “简单使用”。
这里只介绍基于注解的声明式缓存使用,它还支持基于xml
的,并且从 4.1 开始,它还支持 JCache
标准注解。
@Cacheable
:标志着方法调用的结果可以被缓存,如果注解在类上,则代表该类的所有方法的执行结果都可以被缓存。@CacheEvict
:标志着方法将会触发org.springframework.cache.Cache
的evict
方法(任然可以注解在类上),通俗地来说,就是移除缓存。@CachePut
:标志着方法将会触发cache
的put
方法。与@Cacheable
注解所不同的是,该注解标识的方法并不会跳过执行,它总是保存方法执行的结果到关联的缓存中。@Caching
:缓存注解的组注解。@CacheConfig
:提供了一种在类级别上共用的缓存配置。它将给被注解类的任何缓存操作提供默认设置。
用例:操作 ”student“ 实体,以下部分代码,均建立在启动类上已经添加 @EnableCaching
注解,并且类路径下不存在其它特定的缓存库实现。
@Cacheable
@Override
@Cacheable("students")
public Student getStudentById(String id){
for (Student student : container) {
if(student.getId().equals(id)){
return student;
}
}
return null;
}
在可调试模式下,为上面的代码打上断点,基于相同的访问时,只会执行一次。
如果你想了解的更多,你可以通过注入 ApplicationContext
bean 来查看 cacheManager
这个 bean,看一下缓存的具体实现到底是怎样的。下面计算来自于第一次执行 getStudentById
方法体的时候:
如果不会该注解添加 value
值或者 cacheNames
值,你将得到一个错误;从上面的图片也能发现在第一次进入方法体执行之前,需要为抽象的缓存构建具体的存储,需要 “students” 这个 key。
以下是在依次请求了 id 为 1,2以后,此次请求id为 3时,在方法体内断点时,重新计算的 cacheManager
。
我们的暂时抛弃以上 key 这个概念,它所代表的是一个 Cache
的具体实现。我们依赖它在众多 @Cacheable
注解标记的方法中,找到具体的 Cache
实现。那么如何在单个方法的多次调用定位到缓存的值呢?这才是 Spring 所说的 “key”。
- 如果方法没有任何参数,将使用
SimpleKey.EMPTY
- 如果仅仅只有一个参数,将使用它的实例
- 如果有多个参数,将使用
SimpleKey
包含所有的参数。
可以参考
SimpleKey
这个类,理解它如何处理参数,来生成 key 的。以上的方法适用于大多数用例,只要参数具有自然键并实现有效的hashCode()和equals()方法。如果不是这样,你需要改变策略,自己实现org.springframework.cache.interceptor.KeyGenerator
接口。
如何实现自定义key生成
拓展以上方法
@Override
@Cacheable(cacheNames = "studentsOfTeacher", keyGenerator = "keyGeneratorForTeacher")
public List<Student> getStudentByTeacher(Teacher teacher){
List<Student> findStudents = new ArrayList<>();
for (Student student : container) {
if(student.getName().equals(teacher.getName())){
findStudents.add(student);
}
}
return findStudents;
}
在使用自定义 key 生成之前,你需要确保通过 “key” 属性,无法完成你的需求,否则,没有必要做负责的工作。如果使用
key
属性(支持 SpEL), 我们仅仅需要修改@Cacheable
:
@Cacheable(cacheNames = "studentsOfTeacher", key="#teacher.name")
KeyGenerator
接口的实现如下:
@Component("keyGeneratorForTeacher")
public class KeyGeneratorForTeacher implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
if(params.length != 1){
throw new IllegalArgumentException("i just need one param!");
}
if(params[0] instanceof Teacher){
Teacher teacher = (Teacher) params[0];
return teacher.getName();
}
return method.getName() + Arrays.stream(params)
.map(Object::toString).collect(Collectors.joining(","));
}
}
需要注意的是在第一次请求该方法时,以上 key
的生成策略会被调用两次;第二次请求该方法(使用相同的参数),生成策略仅仅只会被调用一次。
该注解还支持使用其它 CacheManager
的实现(cacheManager
属性),甚至支持自定义如何从上下文中解析出 Cache
(cacheResolver
属性)。
如何应对多线程环境
在一个多线程环境中,某些操作可能并发调用相同的点(通常在启动时)。默认情况下,缓存抽象不锁,相同的值可能会计算几次。
对于这些特定的情况下,可以使用 sync
属性指示底层的缓存提供者锁定缓存条目。结果只会有一个线程正在忙着计算值, 而其他人则将被阻塞,直到该条目更新缓存。
这是一个可选的特性,某些缓存库可能不支持它。
如何实现只缓存某些特定的调用(基于参数或者调用结果)
condition
和 unless
可以很好的帮助你完成这项需求。unless
参数否决向缓存添加值的操作。与 condition
不同,unless
表达式是在方法被调用后求值的。
@CachePut
当需要在不影响方法执行的情况下更新缓存时,可以使用 @CachePut
注释。也就是说,方法总是被执行,它的结果被放到缓存中(根据 @CachePut
选项)。它支持与 @Cacheable
相同的选项,应该用于缓存填充而不是方法流优化。
并不应该讲
@CachePut
和@Cacheable
声明在同一个方法上,它们具有不同的行为。
@CacheEvict
此注解用于从缓存中删除过时或未使用的数据。由于方法仅仅充当触发器,返回值将被忽略;但如果方法中抛出了异常,此时,你并仍然想触发缓存清除操作怎么办?我们可以通过 beforeInvocation
这个属性来解决。某种时刻,你想清楚缓存的所有条目,可以使用 allEntries
属性来解决。
@Caching
有时,需要指定同一类型的多个注释(如 @CacheEvict
或 @CachePut
)—例如,因为条件或键表达式在不同的缓存之间是不同的。@Caching
允许在同一方法上使用多个嵌套的 @Cacheable
、@CachePut
和 @CacheEvict
注释。
@CachConfig
存操作提供了许多定制选项,您可以为每个操作设置这些选项。但是,如果某些自定义选项应用于类的所有操作,那么配置它们可能会很繁琐。例如,为类的每个缓存操作指定要使用的缓存名称可以由单个类级别定义替换。这就是 @CacheConfig
发挥作用的地方。
操作级定制总是覆盖 @CacheConfig
上的定制集。因此,这为每个缓存操作提供了三层定制:
-
全局配置,可用的
CacheManager
,KeyGenerator
。 -
在类级别,使用
@CacheConfig
。 -
在操作层面。
总结
缓存的使用是非常简单的,多个注解拥有大多重复的属性,并且由于它的 透明性(应用程序),我们可以不依赖某种特定的缓存库来写代码。对于需要应用缓存的方法,应保证相同的参数调用,结果是一样的,否则,使用缓存就没有什么意义了。在多进程环境下,缓存的支持就和特定的缓存库实现有关,这个需要注意。