Spring缓存抽象
Spring从3.1开始定义了一系列抽象接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们进行缓存开发。Spring Cache 只负责维护抽象层,具体的实现由你的技术选型来决定。将缓存处理和缓存技术解除耦合。
依赖引入
Spring cache 抽象由spring-context相关组件实现。非Spring Boot 项目可通过引入该模块进行集成。
Spring Boot 项目可引入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
同时可能需要引入你采用的缓存中间件客户端;比如 Ehcache、redis等。
两个重要抽象概念
- Cache 缓存抽象规范接口,定义缓存一些了操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
- CacheManager 缓存管理器,管理Cache的生命周期。
常用的一些注解
Spring cache 提供了一系列的注解,将我们从编程开发中解放出来。让我们更加关注于业务开发。
@EnableCaching
该注解是启用Spring cache 的开关。必须开启才能使用Spring cache相关功能。
@CacheConfig
作用于缓存接口上,来对该接口下的一些重复配置(缓存名称、key生成器、缓存管理器、缓存处理器)进行归纳处理。其他属性可参考Cacheable。
@Cacheable(类,方法)
可以标记在一个方法或者类上。方法级只针对该方法。类上则针对类内所有的方法。在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果。
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
//和value注解差不多,二选一
String[] cacheNames() default {};
// 该次缓存的key
String key() default "";
//key的生成器。key/keyGenerator二选一使用
String keyGenerator() default "";
//指定缓存管理器 一般使用默认
String cacheManager() default "";
//或者指定获取解析器 一般使用默认
String cacheResolver() default "";
//条件符合则缓存 使用的比较多 支持SpEL
String condition() default "";
//条件符合则不缓存 使用的比较多 支持SpEL
String unless() default "";
//是否使用异步模式
boolean sync() default false;
}
后面有个别注解属性跟这个基本相同不进行重复介绍。
key/keyGenerator
- SpringEL表达式
1.#参数名”或者“#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.Spring还为我们提供了一个root对象可以用来生成key
- Spring默认的SimpleKeyGenerator
Spring默认的SimpleKeyGenerator是不会将函数名组合进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().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
@Cacheable(value="gomeo2oCache", keyGenerator = "keyGenerator")
public ResultDTO method(User user);
@CachePut(类,方法)
public @interface CachePut {
String[] value(); //缓存的名字,可以把数据写到多个缓存
String key() default ""; //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍
String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断
String unless() default ""; //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了
}
该注解容易跟@Cacheable混淆。两者都可以执行缓存的“放入”操作,不同于@Cacheable,@CachePut每在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中,如果该K存在则进行更新。其他属性可参考Cacheable。
@CacheEvict(类,方法)
public @interface CacheEvict {
String[] value(); //请参考@CachePut
String key() default ""; //请参考@CachePut
String condition() default ""; //请参考@CachePut
boolean allEntries() default false; //是否移除所有数据
boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除
@CachEvict主要针对方法配置,能够根据一定的条件对特定的缓存进行清空。该注解有两个特别的属性:
- value 表示清除操作是发生在哪些Cache上的(对应Cache的名称)
- key 表示需要清除的是哪个key,如未指定则会使用默认策略生成的key
- condition 表示清除操作发生的条件
- allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。注意不能跟key参数同时使用。
- beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。
@Caching
- cacheable
- put
- evict
该注解是个组合注解。有时候我们需要在一个方法上同时使用多个相同注解但是java是不支持一个注解在同一个方法上多次使用。这时就可以使用该注解进行组合。
@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
@CacheEvict(value = "cache3", allEntries = true) })
public User find(Integer id) {
return null;
}
运行流程
- 首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有
- 接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)
- 如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)
- 如果没有找到缓存,那么调用实际的API,把结果放入result
- 如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests
- 执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);
- 执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有
流程中需要注意的就是2/3/4步:
如果有@CachePut操作,即使有@Cacheable也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut和@Cacheable;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景)。
自定义注解
Spring允许我们在配置可缓存的方法时使用自定义的注解,前提是自定义的注解上必须使用对应的注解进行标注。如我们有如下这么一个使用@Cacheable进行标注的自定义注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(value="users")
public @interface MyCacheable {
}
@MyCacheable
public User findById(Integer id) {
System.out.println("find user by id: " + id);
User user = new User();
user.setId(id);
user.setName("Name" + id);
return user;
}
使用要点
- 缓存注解所在的方法不能在类中进行内部调用。
- 缓存一定要有过期超时策略,避免系统不堪重负。
- 缓存的值如果是集合考虑对集合的大小的限制,避免序列化/反序列化性能。