Spring 从3.1开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用 JCache(JSR-107)注解简化我们开发。SpringCache本质上不是一种缓存的实现,而是一种缓存的抽象[1]。
Spring Cache 的使用方法和原类似于 Spring 对事务管理的支持,都是 AOP 的方式。其核心思想是:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回[2]。
在Spring Boot 环境下,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在配置类使用@EnableCaching 开启缓存支持即可。
(1)提供基本的cache抽象,方便切换各种底层cache
(2)通过注解Cache可以实现逻辑代码透明缓存
(3)支持事务回滚时也自动回滚缓存
(4)支持复杂的缓存逻辑
(5)支持各种缓存实现,默认基于ConcurrentMap实现的ConcurrentMapCache,同时支持其他缓存实现
(6)支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
(1)不支持TTL,即不能设置过期时间 expires time,SpringCache 认为这是各个Cache实现自己去完成的事情,有方案但是只能设置统一的过期时间,明显不够灵活。
(2)内部调用,非 public 方法上使用注解,会导致缓存无效。内部调用方法的时候不会调用cache方法。
由于 SpringCache 是基于 Spring AOP 的动态代理实现,由于代理本身的问题,当同一个类中调用另一个方法,会导致另一个方法的缓存不能使用,这个在编码上需要注意,避免在同一个类中这样调用。如果非要这样做,可以通过再次代理调用,如 ((Category)AopContext.currentProxy()).get(category) 这样避免缓存无效。
(3)key的问题。在清除缓存的时候,无法指定多个缓存块,同时清除多个缓存的key。
(1)@Cacheable标注的方法,如果其所在的类实现了某一个接口,那么该方法也必须出现在接口里面,否则cache无效。
原因:Spring把实现类装载成为Bean的时候,会用代理包装一下,所以从Spring Bean的角度看,只有接口里面的方法是可见的,其它的都隐藏了,自然看不到实现类里面的非接口方法,@Cacheable不起作用。
(2)不要在抽象类(例如Repository)和接口中使用@Cache*注解。
(3)建议不缓存分页查询的结果
在做分页查询时,查询出来的内容只是所有内容的一部分,比如当分页条件pageSize=10,pageNum=1时,查了前10条数据,存入缓存。当pageSize=20,pageNum=1时,查了前20条数据,也存入了缓存。这样缓存中前10条数据就重复存在了,增大了内存负担。
(4)基于 proxy 的 spring aop 带来的内部调用问题
假设对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么切面就失效,也就是说 @Cacheable、@CachePut 和 @CacheEvict 都会失效。
(5) 非 public 方法问题
@Cache*注解的方法必须为public,否则会报错
(6)@CacheEvict的可靠性
默认情况下,如果方法执行期间抛出异常,则不会清空缓存。
Cache:缓存接口,定义缓存操作。实现有:RedisCache、CaffeineCache、ConcurrentMapCache等
CacheManager:缓存管理器,管理各种缓存(cache)组件
keyGenerator:缓存数据时key生成策略,和 key 互斥,不能共存
serialize:缓存数据时value序列化策略
名称 | 解释 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对结果其进行缓存 |
@CachePut | 主要针对方法配置,根据方法的请求参数对其结果进行缓存和@Cacheable不同的是,每次都会触发真实方法的调用 |
@CacheEvict | 主要针对方法配置,能够根据一定的条件对缓存进行清空 |
@EnableCaching | 针对启动类,开启基于注解的缓存 |
@Caching | 用来组合使用其他解,可以同时应用多个Cache注解。下面我们分别简单介绍 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
value
缓存注释的属性不再是必须的[9]。**尽管在大多数情况下,仅声明一个缓存,但注释可指定多个名称,以便使用多个缓存。在这种情况下,在执行该方法之前会检查每个缓存-如果命中了至少一个缓存,则返回关联的值。即使未实际执行缓存的方法,所有其他不包含该值的缓存也会被更新。[9]。**例如:
@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()<32")
unless: 否定缓存。当条件结果为TRUE时,就不会缓存。与condition不同的是,unless表达式是在方法调用之后进行评估的。如果返回false,才放入缓存(与condition相反)。#result指返回值。例如:
@Cacheable(value="testcache", unless="#result.userName.length()>2")
public User findByName(String userName)
allEntries(@CacheEvict):是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。例如:
@CachEvict(value="testcache", allEntries=true)
beforeInvocation(@CacheEvict):是否在方法执行前就清空,缺省为 false,如果指定为 true,
则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。例如:
@CachEvict(value="testcache", beforeInvocation=true)
(1) 上下文数据
Spring Cache提供了一些供我们使用的SpEL上下文数据:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
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 | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
注意:
@Cacheable(key = "targetClass + methodName + #p0")
@Cacheable(value="users", key="#id")
@Cacheable(value="users", key="#p0")
(2) 运算符
SpEL提供了多种运算符[10]
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&,||,!,and,or,not,between,instanceof |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],1,$[…] |
环境:Spring boot 2.x.x
开始使用前需要导入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
在启动类注解**@EnableCaching**开启缓存:
@SpringBootApplication
@EnableCaching//开启缓存
public class TestApplication{
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
作用:注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。注意:要缓存的实体类必须实现序列化。一般用于查找
属性:
名称 | 类型 | 默认值 | 解释 |
---|---|---|---|
value | String[] | {} | 缓存名,必填,至少指定一个,指定缓存存放在哪块命名空间 |
cacheNames | String[] | {} | value别名,和value二选一 |
key | String | “” | 可选属性,可以使用SpEL标签自定义缓存的key |
keyGenerator | String | “” | key的生成器。key/keyGenerator二选一使用 |
cacheManager | String | “” | 指定缓存管理器 |
cacheResolver | String | “” | 指定获取解析器 |
condition | String | “” | 条件符合则缓存 |
unless | String | “” | 条件符合则不缓存 |
sync | boolean | false | 是否使用异步模式 |
@Cacheable("userList")//标识读缓存操作。
public List<User> findAll(){
return userService.findAll();
}
//如果缓存存在,直接读取缓存值,如果缓存不存在,则调用目标方法,并将方法返回结果放入缓存。
@Cacheable(value = "user", key = "#id")
//@Cacheable(value = "user", key = "targetClass + methodName + #p0")
public User findOne(Long id){
return userService.findOne(id);
}
问题:
1、缓存列表数据时,怎么保证单条数据更新后,列表同步更新
作用:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。但需要注意的是该注解的 value 和 key 必须与要更新的缓存相同,也就是与 @Cacheable 相同。一般用于更新和新增。
属性:
名称 | 类型 | 默认值 | 解释 |
---|---|---|---|
value | String[] | {} | 缓存名,必填,至少指定一个,指定缓存存放在哪块命名空间 |
cacheNames | String[] | {} | value别名,和value二选一 |
key | String | “” | 可选属性,可以使用SpEL标签自定义缓存的key |
keyGenerator | String | “” | key的生成器。key/keyGenerator二选一使用 |
cacheManager | String | “” | 指定缓存管理器 |
cacheResolver | String | “” | 指定获取解析器 |
condition | String | “” | 条件符合则缓存 |
unless | String | “” | 条件符合则不缓存 |
示例代码:
@CachePut(value="user", key="#user.name")//写入缓存
//@CachePut(value="user", key="targetClass + #p0")
public User saveUser(User user){
return userSevice.save(user);
}
@CachePut(value="user", key="#user.id")
//@CachePut(value="user", key="targetClass + #p0")
public User updateUser(User user){
return userSevice.update(user);
}
名称 | 类型 | 默认值 | 解释 |
---|---|---|---|
value | String[] | {} | 缓存名,必填,至少指定一个,指定缓存存放在哪块命名空间 |
cacheNames | String[] | {} | value别名,和value二选一 |
key | String | “” | 可选属性,可以使用SpEL标签自定义缓存的key |
keyGenerator | String | “” | key的生成器。key/keyGenerator二选一使用 |
cacheManager | String | “” | 指定缓存管理器 |
cacheResolver | String | “” | 指定获取解析器 |
condition | String | “” | 条件符合则缓存 |
allEntries | boolean | false | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 |
beforeInvocation | boolean | false | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存。默认情况下,如果方法执行抛出异常,则不会清空缓存。 |
@CacheEvict(value="user", key="#id")//清除一条缓存,key为要清空的数据
public Boolean deleteById(Long id){
userService.deleteById(id);
}
//方法调用后清空所有缓存
@CacheEvict(value="userCache", allEntries=true)
public void delectAll() {
userService.deleteAll();
}
//方法调用前清空所有缓存
@CacheEvict(value="accountCache", beforeInvocation=true)
public void delectAll() {
userService.deleteAll();
}
作用:有时候可能在同一个方法上组合多个Cache注解使用,此时就需要@Caching组合多个注解标签了。
属性:
名称 | 类型 | 默认值 |
---|---|---|
cacheable | Cacheable[] | {} |
put | CachePut[] | {} |
evict | CacheEvict[] | {} |
@Caching(evict={@CacheEvict("user1"), @CacheEvict("user2",allEntries=true)})
作用:当我们需要缓存的地方越来越多,可以在类上使用 @CacheConfig(cacheNames = {""}) 注解来统一指定 value 的值,这时方法可省略 value,如果你在你的方法依旧写上了 value,那么依然以方法的value值为准。
@CacheConfig是一个类级别的注解,它允许共享缓存名称,自定义KeyGenerator,自定义CacheManager和自定义CacheResolver。 将此注释放在类上不会开启任何缓存操作[9]。
属性:
名称 | 类型 | 默认值 | 解释 |
---|---|---|---|
cacheNames | String[] | {} | 缓存名,同value |
keyGenerator | String | “” | key的生成器,同key |
cacheManager | String | “” | 指定缓存管理器 |
cacheResolver | String | “” | 指定获取解析器 |
@CacheConfig(cacheNames = {"myCache"})
public class UserServiceImpl implements UserService {
@Override
@Cacheable(key = "targetClass + methodName + #p0")//此处没写value
public List<User> findAllLimit(int num) {
return userRepository.findAllLimit(num);
}
.....
}
Spring支持的CacheManager:
图1 Spring支持的CacheManager
SimpleCache:没有引入其他缓存组件的情况下,SpringBoot 的默认缓存,使用ConcurrentMap实现.
GuavaCache:单个应用运行时的本地缓存。与 ConcurrentMap 很相似,但不同的是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。而 Guava Cache为 了限制内存占用,通常都设定为自动回收元素。
CaffeineCache:使用Java8对Guava缓存的重写版本,在Spring Boot 2.0 中取代,基于LRU算法实现,支持多种缓存过期策略。
EhCacheCache:是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的 CacheProvider。
RedisCache:读写速度快,分布式。
(1)Spring cache 是代码级的缓存,一般是使用一个 ConcurrentMap,也就是说实际上还是是使用 JVM 的内存来缓存对象的,这势必会造成大量的内存消耗。但好处是显然的:使用方便。
(2)Redis 作为一个缓存服务器,是内存级的缓存。它是使用单纯的内存来进行缓存。
(3)集群环境下,每台服务器的 Spring Cache 是不同步的,这样会出问题的,Spring Cache 只适合单机环境。
(4)Redis是设置单独的缓存服务器,所有集群服务器统一访问 Redis,不会出现缓存不同步的情况。
(5)存储数据安全–Cache挂掉后,数据丢失且不可恢复;Redis可以定期保存到磁盘(持久化)。
LRU,LFU,FIFO
LRU (Least recently used):最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
LFU (Least frequently used): 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
FIFO (Fist in first out): 先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉。
适合缓存的数据:
自定义配置:CacheManager、CacheConfig
SimpleCache 是 SpringBoot 的默认缓存实现,在没有引入其他缓存组件的情况下,SpringBoot 默认加载SimpleCacheConfiguration进行自动配置[5]。SimpleCache 实现方式是使用 ConcruentMap。
主要是注册一个 ConcruentMap 缓存管理器组件
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();//默认使用ConcurrentMapCacheManager实现
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
(1)引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
(2)启动类加 **@EnableCaching ** 注解
Caffeine 提供了三种缓存填充策略:手动、同步加载和异步加载[6]。
在Caffeine中分为两种缓存,一个是有界缓存,一个是无界缓存,无界缓存不需要过期并且没有界限。在有界缓存中提供了三个过期API:
LoadingCache<String, String> build = CacheBuilder.newBuilder().initialCapacity(1).maximumSize(100).expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, String>() {
//默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
@Override
public String load(String key) {
return "";
}
});
参数说明:
设定多长时间后会自动刷新缓存。
Caffeine提供了refreshAfterWrite()方法来让我们进行写后多久更新策略:
LoadingCache<String, String> build = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return "";
}
});
}
Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)。
移除监听器(Removal),如果需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。
统计(Statistics),使用Caffeine.recordStats(),可以打开统计信息收集。Cache.stats() 方法返回提供统计信息的CacheStats,如:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>com.github.ben-manes.caffeinegroupId>
<artifactId>caffeineartifactId>
<version>2.6.2version>
dependency>
在properties配置文件设置使用的缓存类型为caffeine
spring.cache.type=caffeine
Redis的缓存实现:
spring-boot 的不同版本,都需要先配置 redis 连接,两个版本的 redis 客户端连接池使用有所不同。
spring-boot 版本:1.5.x,默认客户端类型为 jedis
spring-boot 版本:2.x,默认客户端类型为 lettuce
1.添加依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
2.redis配置信息:
在SpringBoot的application.yml配置文件中配置redis数据库的相关信息,这里改动主要有两点,其一是时间相关的属性,如spring.redis.timeout,在1.0中,时间相关的配置参数类型为int,默认单位为毫秒,配置中只需指定具体的数字即可,而在2.0中,时间相关的配置的参数类型都改为了jdk1.8的Duration,因此在配置文件中配置redis的连接超时时间timeout时,需要加入时间单位,如60s;其二是,在2.0中配置redis的连接池信息时,不再使用spring.redis.pool的属性,而是直接使用redis的lettuce或jedis客户端来配置,具体配置信息如下[7]:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
spring.redis.timeOut=60s
#lettuce客户端
spring.redis.lettuce.pool.min-idle=0
# 最大空闲连接数
spring.redis.lettuce.pool.max-idle=8
# 等待可用连接的最大时间,负数为不限制
spring.redis.lettuce.pool.max-wait=-1ms
# 最大活跃连接数,负数为不限制
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.shutdown-timeout=100ms
...
3.在配置文件设置使用的缓存类型为redis
spring.cache.type=redis
4.在启动类添加注解**@EnableCaching**开启缓存
5.方法上添加注解
参考资料:
[1].史上最全的Spring Boot Cache使用与整合
https://www.cnblogs.com/yueshutong/p/9381540.html
[2].陈剑光.《Spring Boot开发实战》[M].北京:机械工业出版社,2018:278-287.
https://www.jb51.net/books/691682.html
[3].SpringBoot官方文档
https://docs.spring.io/spring-boot/docs/2.1.4.RELEASE/api/
org.springframework.boot.autoconfigure.cache
[4].Spring官方文档
https://docs.spring.io/spring/docs/5.1.7.RELEASE/javadoc-api/
org.springframework.cache*
[5].SpringBoot原生缓存,SimpleCacheConfiguration解读,以及注解使用
https://blog.csdn.net/shijiujiu33/article/details/90708498
[6].Caffeine Cache 进程缓存之王
https://www.jianshu.com/p/15d0a9ce37dd
[7].SpringBoot 2.x 整合 redis 做缓存
https://blog.csdn.net/sy793314598/article/details/80719224
[8].Spring官方简单案例:Caching Data with Spring
https://spring.io/guides/gs/caching/
[9].Spring官方文档缓存抽象具体介绍:8.Cache Abstraction
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
[10].更多SpEL介绍:Spring Expression Language (SpEL)
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions-evaluation
[11].有关实现所需的高级定制(使用Java配置)的更多详细信息CachingConfigurer
[9]
https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html
[12].Caffiene 文档[9]
https://github.com/ben-manes/caffeine/wiki
… ↩︎