一、前言
SpringCache
是Spring
提供的一个缓存框架,在Spring 3.1
版本开始支持将缓存添加到现有的Spring
应用程序中,在4.1
开始,缓存已支持JSR-107
注释和更多自定义的选项。
Spring Cache
利用了AOP
,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了,做到了对代码侵入性做小。
由于市面上的缓存工具实在太多,SpringCache
框架还提供了CacheManager
接口,可以实现降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine
、Guava Cache
、Ehcache
、Redis
等。
结合上篇:本地缓存解决方案Caffeine | Spring Cloud 38 来实现Spring Boot
使用Caffeine
实现缓存。
二、SpringCache核心概念
2.1 缓存注解
-
Cache
接口:缓存接口,定义缓存操作。实现有如:RedisCache
、CaffeineCache
等 -
cacheResolver
:指定缓存解析器 -
CacheManager
:缓存管理器,管理各种缓存(Cache
)组件;如:RedisCacheManager
使用Redis
作为缓存、CaffeineCacheManager
使用Caffeine
作为缓存 -
@Cacheable
:在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。 -
@CacheEvict
:将一条或多条数据从缓存中删除。 -
@CachePut
:将方法的返回值放到缓存中 -
@EnableCaching
:开启缓存注解功能 -
@Caching
:组合多个缓存注解; -
@CacheConfig
:统一配置@Cacheable
中的value
值
2.2 缓存注解主要参数
主要针对缓存操作注解:
@Cacheable
、@CachePut
、@CacheEvict
。
名称 | 解释 |
---|---|
value |
缓存的名称,在 Spring 配置文件中定义,必须指定至少一个例如: @Cacheable(value="mycache") 或者@Cacheable(value={"mycache1","mycache2"}) |
key |
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如: @Cacheable(value="mycache",key="#id") |
condition |
对入参进行判断,符合条件的缓存,不符合的不缓存,可以为空, 使用 SpEL 编写,属性范围只有#root 和获取参数类的SpEL表达式 ,不能使用返回结果的 #result (condition = "#result != null" 会导致所有对象都不进入缓存),返回 true 或者 false ,只有为 true 才进行缓存例如: @Cacheable(value="mycache",condition="#id>2") |
unless |
对出参进行判断,符合条件的不缓存,不符合的缓存。当条件结果为true 时,就不会缓存。例如: @Cacheable(value="mycache",unless="#result == null") |
allEntries (@CacheEvict注解) |
是否清空所有缓存内容,缺省为 false ,如果指定为 true ,则方法调用后将立即清空所有缓存例如: @CachEvict(value="mycache",allEntries=true) |
beforeInvocation (@CacheEvict注解) |
是否在方法执行前就清空,缺省为 false ,如果指定为 true ,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存例如: @CachEvict(value="mycache",beforeInvocation=true) |
三、集成使用示例
3.1 集成方式汇总
- 使用
SimpleCacheManager
作为缓存管理器- 特点:
- 这种缓存管理器允许你在应用程序启动时通过配置多个
CaffeineCache
来创建多个缓存。 - 这种方式可以让你为每个方法单独配置缓存过期时间。
- 这种缓存管理器允许你在应用程序启动时通过配置多个
- 配置方式:
- 代码配置
- 特点:
- 使用
CaffeineCacheManager
作为缓存管理器- 特点:
- 这种缓存管理器使用了一个全局的
Caffeine
配置来创建所有的缓存。 - 这种方式不能为每个方法单独配置缓存过期时间,但是可以在程序启动时配置全局的缓存配置,这样就可以轻松地设置所有方法的缓存过期时间。
- 这种缓存管理器使用了一个全局的
- 配置方式:
- 配置文件
- 代码配置
- 特点:
3.2 Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
3.3 SimpleCacheManager集成
3.3.1 定义缓存的过期时间
CacheNameTimeConstant
:
public interface CacheNameConstant {
String CACHE_DEFAULT = "CACHE_DEFAULT";
String CACHE_5SECS = "CACHE_5SECS";
String CACHE_10SECS = "CACHE_10SECS";
String CACHE_30SECS = "CACHE_30SECS";
String USERS = "USERS";
}
3.3.2 缓存配置
CaffeineConfig
:
@Configuration
@EnableCaching
public class CaffeineConfig {
@Bean
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
caches.add(new CaffeineCache(CacheNameConstant.CACHE_5SECS,
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build()));
caches.add(new CaffeineCache(CacheNameConstant.CACHE_10SECS,
Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build()));
caches.add(new CaffeineCache(CacheNameConstant.CACHE_30SECS,
Caffeine.newBuilder().expireAfterWrite(300, TimeUnit.SECONDS).build()));
caches.add(new CaffeineCache(CacheNameConstant.USERS,
Caffeine.newBuilder().expireAfterWrite(120, TimeUnit.SECONDS).build()));
cacheManager.setCaches(caches);
return cacheManager;
}
}
3.4 CaffeineCacheManager集成
3.4.1 配置文件方式
CaffeineSpec
字符格式规范:https://github.com/ben-manes/caffeine/wiki/Specification-zh-CN
CaffeineSpec
为Caffeine
提供了一个简单的字符格式配置。这里的字符串语法是一系列由逗号隔开的键值对组成,其中每个键值对对应一个配置方法。但是这里的字符配置不支持需要对象来作为参数的配置方法,比如 removalListener
,这样的配置必须要在代码中进行配置。
以下是各个配置键值对字符串所对应的配置方法:
initialCapacity=[integer]
: 相当于配置Caffeine.initialCapacity
maximumSize=[long]
: 相当于配置Caffeine.maximumSize
maximumWeight=[long]
: 相当于配置Caffeine.maximumWeight
expireAfterAccess=[持续时间]
: 相当于配置Caffeine.expireAfterAccess
expireAfterWrite=[持续时间]
: 相当于配置Caffeine.expireAfterWrite
refreshAfterWrite=[持续时间]
: 相当于配置Caffeine.refreshAfterWrite
weakKeys
: 相当于配置Caffeine.weakKeys
weakValues
: 相当于配置Caffeine.weakValues
softValues
: 相当于配置Caffeine.softValues
recordStats
: 相当于配置Caffeine.recordStats
持续时间可以通过在一个
integer
类型之后跟上一个"d","h","m",
或者"s"
来分别表示天,小时,分钟或者秒。
将
maximumSize
和maximumWeight
或者将weakKeys
和weakValues
在一起使用是不被允许的。
配置文件示例:
server:
port: 6606
spring:
cache:
type: caffeine
cache-names: USERS
caffeine:
spec: initialCapacity=50,maximumSize=256,expireAfterWrite=300s,refreshAfterWrite=300s,recordStats
cache-names
:多个缓存器名称请用,
分割。
3.4.2 代码配置方式
CaffeineConfig
:
@Bean
public CacheManager cacheManager() {
Caffeine<Object, Object> caffeineCache = caffeineCache();
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeineCache);
// 设定缓存器名称
cacheManager.setCacheNames(getNames());
// 值不可为空
cacheManager.setAllowNullValues(false);
return cacheManager;
}
private static List<String> getNames() {
List<String> names = new ArrayList<>(1);
names.add("USERS");
return names;
}
@Bean
public Caffeine<Object, Object> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000);
}
/**
* Caused by: java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache
*
* @return
*/
@Bean
public CacheLoader<Object, Object> cacheLoader() {
return this::loadData;
}
/**
* 根据key加载缓存元素
* @param key
* @return
*/
private Object loadData(Object key) {
return null;
}
3.5 外部获取缓存值
@Autowired
CacheManager cacheManager;
Cache cache = cacheManager.getCache("USERS");
return cache.get(key).get();
其中
USERS
为上文定义的缓存器名称。
四、Spring-Cache的不足
4.1 读模式
4.1.1 缓存穿透
- 问题描述:查询结果返回是
null
数据。 - 解决:缓存空对象
cache-null-values: true
或cacheManager.setAllowNullValues(true)
。
4.1.2 缓存击穿
- 问题描述:大量并发同时查询一个过期的数据。
- 解决:
@Cacheable(sync = true)
通过加锁解决击穿。
4.1.3 缓存雪崩
- 问题描述:缓存集中在一段时间内失效,引发大量缓存穿透,所有的查询都落在数据库上,造成缓存雪崩。
- 解决:使用
SimpleCacheManager
设置多个缓存器并设置不同的过期时间,使用时按需指定不同的缓存器,让缓存失效的时间尽量分布均匀。
五、附录
5.1 SpEL上下文
Spring Cache
提供了一些供我们使用的SpEL
上下文数据,下表直接摘自Spring
官方文档:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName |
root 对象 |
当前被调用的方法名 | #root.methodname |
method |
root 对象 |
当前被调用的方法 | #root.method.name |
target |
root 对象 |
当前被调用的目标对象实例 | #root.target |
targetClass |
root 对象 |
当前被调用的目标对象的类 | #root.targetClass |
args |
root 对象 |
当前被调用的方法的参数列表 | #root.args[0] |
caches |
root 对象 |
当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name |
执行上下文 | 当前被调用的方法的参数,如find(User user) ,可以通过#user.id 获得参数 |
#user.id |
result |
执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如: unless 或 cacheEvict 的beforeInvocation=false ) |
#result |
表达式示例:
使用参数作为
key
:使用方法参数时我们可以直接使用#参数名
或者#p参数Index
@Cacheable(value = "users", key = "#id")
public User find(Integer id) {
return null;
}
@Cacheable(value = "users", key = "#p0")
public User find(Integer id) {
return null;
}
@Cacheable(value = "users", key = "#user.id")
public User find(User user) {
return null;
}
@Cacheable(value = "users", key = "#p0.id")
public User find(User user) {
return null;
}
5.2 SpEL运算符
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | `&&, |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],^[…],$[…] |