【Spring】Spring Cache

简介

https://zhuanlan.zhihu.com/p/266804094

为在项目中使用缓存技术(例如 Redis),我们通常要将添加缓存的代码耦合到业务代码中,这样每个业务代码都需要添加重复的缓存代码。自然可以想到,使用 Spring AOP 的思想进行解耦。

Spring Cache 就是这么一个框架。它利用了 Spring AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解并配置缓存框架的类型,就能实现缓存功能了。而且 Spring Cache 也提供了很多默认的配置,用户可以为自己的业务代码快速加上一个很不错的缓存功能。

官网地址:https://docs.spring.io/spring-framework/docs/5.2.10.RELEASE/spring-framework-reference/integration.html#cache-strategie

Spring 从3.1开始定义了 org.springframework.cache.Cacheorg.sprngframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用 JCache(JSR-107)注解简化我们的开发。

CacheManager 接口为缓存的组件定义存储规则,其内部的缓存组件(Cache 接口类型)才是真正向缓存中存储数据的(以 k:v 存储数据)。这些组件根据名字进行区分,存储的规则是由 CacheManager 制定的。例如:

  • 如果使用ConcrrentMapCache 类型的 CacheManager,则其内部存储的 ConcrrentMapCache 里面都是使用 ConcurrentMap 存储数据的,通常用于本地缓存。
  • 如果使用 RedisCache类型的 RedisCacheManager,则其内部存储的 RedisCache对象存储的数据都是存放在 Redis 中的,通常用于分布式缓存。
image-20220109113102413

从图中可以看到,不同的 CacheManager 中保存着不同类型的 Cache,并且这些 Cache 都有自己的名字(在 Redis 中显示的 Key 的名字),并且其内按照 k:v 的方式保存数据。

RedisCache 为例。使用 Spring Cache 时,在方法上标注注解 @Cacheable。这样每次在调用该方法前会向 Redis 检查该缓存是否已存在:

  • 如果已存在就直接从缓存中获取方法调用后的结果,方法内代码将不再被执行
  • 如果不存在再执行方法内代码并将方法的返回结果到 Redis 中,下次就可以直接调用从缓存中获取该数据了

原理

引入 Spring Cache 的场景启动器后,其将注入一个 CacheAutoConfiguration的自动配置类:

image-20220109112149406

该自动配置类将注入一些缓存框架的自动配置类,例如 RedisAutoConfiguration

image-20220109112440209

并且 Spring Cache 自动配置了 RedisCacheManagerRedisCacheConfiguration ,用于设置 Redis 缓存的各个配置参数。如果容器中有开发人员编写的 RedisCacheConfiguration,就使用。否则就使用默认提供的配置参数。

若开发人员想自定义缓存的配置,只需要向容器中注入一个自定义的 RedisCacheConfiguration 并在其内设置缓存参数即可,这样这些参数就会应用到当前 RedisCacheManager 管理所有缓存分区中。

image-20220109122600333

快速使用

  1. 导入 Maven 依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  1. 在启动类加上 @EnableCaching 注解即可开启使用缓存
1
2
3
4
5
6
7
@SpringBootApplication
@EnableCaching
public class CachingApplication {
public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}
}
  1. 配置使用 Redis 进行缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
redis:
host: yuyunzhao.cn
port: 6379
password: zhaoyuyun # 设置密码防止被别人利用
cache:
type: redis # 配置使用 Redis 进行缓存
# cache-names: # 如果没配名字,就按照系统中用到的缓存进行起名,一般不设置
redis:
time-to-live: 360000 # 设置过期时间,单位是 ms
# key-prefix: CACHE_ # key 前缀。推荐不指定前缀,这样分区名(value)默认就是缓存的前缀,有利于分级展示 key 的信息
use-key-prefix: true # 是否使用前缀(开启后,默认使用 @Cacheable 中指定的分区名作为前缀)
cache-null-values: true # 是否允许缓存空值,可用于防止缓存穿透
  1. 在要缓存的方法上面添加 @Cacheable 注解,即可缓存这个方法的返回值
1
2
3
4
5
6
7
@Cacheable(value = {"category", "product"}, key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
// 如果缓存中没有才会执行该方法内的代码,否则直接返回缓存中的结果
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}

下面详细介绍 Spring Cache 里的常用注解。

常用注解

Spring Cache有几个常用注解,分别为@Cacheable@CachePut@CacheEvict@Caching@CacheConfig。除了最后一个CacheConfig 外,其余四个都可以用在类上或者方法级别上,如果用在类上,就是对该类的所有public方法生效,下面分别介绍一下这几个注解。

@Cacheable

@Cacheble注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
@Override
public List<CategoryEntity> getCategoryLevel1() {
System.out.println("方法执行了");
return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

@Cacheable(value = {"category", "product"}, key = "'getCatalogJson'")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 不需要再手动写缓存的代码了
return getCatalogJsonFromDB();
}

该注解的参数:

  • value:同 cacheNames。缓存的分区名称。如果不指定 key-prefix 时,就会作为缓存名称的前半部分(即前缀)
  • key缓存名称。如果不指定 key-prefix 时,就会以 value 为前缀拼接在一起,共同组成在 Redis 中的缓存 key 值。例如上例中在 Redis 里保存的缓存数据的key 值为 category::getCatalogJson
  • sync:为 true 代表开启本地锁(注意不是分布式锁)。默认是不开启的。可用该参数缓解缓存击穿问题。开启后只能指定唯一一个 value。

key 的取值采用 Spring Expression Language (SpEL) 规则,如果想直接取值需要加 ‘xxx’。否则要使用 #root.xxxx 进行动态取值。并且 key 只能写单个值,不可以指定多个 key

注意:当配置文件中不显式指定前缀 key-prefix 时,就会使用该注解中的 value 值作为前缀,这样最终保存在 Redis 里的缓存的 key 就是 value 值(分区名):: key 值(缓存名)。上例中的两个缓存数据在 Redis 里的显示效果为:

image-20220108213246545

其中 categoryproduct 是分区名(value 值),其后的 getCatalogJsongetCategoryLevel1 是缓存名(key 值)。


@Cacheable 的默认配置:

  • 如果缓存中有,方法就不再调用
  • key 如果不显式指定,则默认自动生成为:缓存的区分:SimpleKey
  • 缓存中存储的值,默认使用 JDK 序列化(若想指定为 JSON 格式需要自定义配置)
  • 默认的过期时间:-1(可自定义)

@CachePut

加了 @CachePut 注解的方法,会把方法的返回值立即 put 到缓存里面缓存起来,供其它地方使用。它通常用在新增方法上双写模式可以使用该注解,要求方法必须有返回值,该注解会将该返回值存到缓存中。

1
2
3
4
5
6
7
8
@CachePut
@Transactional
@Override
public CategoryEntity addCascade(CategoryEntity category) {
this.add(category);
// 要求必须有返回值
return category;
}

@CacheEvict

使用了 @CacheEvict 注解的方法,会在执行完方法内容后清空指定缓存。一般用在更新或者删除的方法上失效模式可以使用该注解。方法不需要返回值。

1
2
3
4
5
6
7
8
9
@CacheEvict(value = {"category", "product"}, key = "'getCategoryLevel1'", allEntries = true) // 注意只能写单个key;并且指定删除某个分区下的所有数据
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
// 先更新商品表
this.updateById(category);
// 更新关联表中的数据
relationService.updateCategoryName(category.getCatId(), category.getName());
}

注意:该注解的 key 只能写单个值,不可以指定多个 key,若想同时指定多个 key,则需要使用 @Caching 注解。

allEntries = true 作用:指定删除某个分区下的所有数据

@Caching

Java注解的机制决定了,一个方法上只能有一个相同的注解生效。那有时候可能一个方法会操作多个缓存(这个在删除缓存操作中比较常见,在添加操作中不太常见)。

Spring Cache当然也考虑到了这种情况,@Caching注解就是用来解决这类情况的,一看它的源码就明白了。

1
2
3
4
5
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}

可以使用该注解组合多个注解,例如:

1
2
3
4
5
6
7
8
9
10
11
12
@Caching(evict = {
@CacheEvict(value = {"category", "product"}, key = "'getCategoryLevel1'"),
@CacheEvict(value = {"category", "product"}, key = "'getCatalogJson'")
})
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
// 先更新商品表
this.updateById(category);
// 更新关联表中的数据
relationService.updateCategoryName(category.getCatId(), category.getName());
}

@CacheConfig

前面提到的四个注解,都是 Spring Cache 常用的注解。每个注解都有很多可以配置的属性。但这几个注解通常都是作用在方法上的,而有些配置可能又是一个类通用的,这种情况就可以使用@CacheConfig了,它是一个类级别的注解,可以在类级别上配置cacheNameskeyGeneratorcacheManagercacheResolver等。

自定义配置缓存参数

若想自定义配置 Spring Cache 的缓存配置,只需要向容器中注入一个自定义的 RedisCacheConfiguration 并在其内设置缓存参数即可,这样这些参数就会应用到当前 RedisCacheManager 管理所有缓存分区中。

需要注意的是,如果不加 @EnableConfigurationProperties(CacheProperties.class) 注解,则该配置类将无法读取配置文件中的相关配置参数。该注解将会使 CacheProperties 对象绑定配置文件中的 cache 配置并使其生效。详细原理见文章 【Spring Boot】Spring Boot2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {

/**
* 默认配置文件中的东西是没有用上的
* 1、原来的配置文件绑定的配置类是这样子的
* @ConfigurationProperties(prefix = "Spring.cache")
* 2、要让他生效的话,必须加上下面注解,将CacheProperties属性与配置文件中的指定前缀内容进行绑定,否则配置文件的内容无法生效
* @EnableConfigurationProperties(CacheProperties.class)
* @param cacheProperties 从容器中自动注入的缓存属性对象,其内绑定了本项目配置文件中的一些属性值
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置key的序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 设置value序列化 ->JackSon,否则会使用默认的JDK序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

// 将配置文件中的所有配置都生效
// 从缓存属性对象中读取配置文件中的自定义属性
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
redis:
host: yuyunzhao.cn
port: 6379
password: zhaoyuyun # 设置密码防止被别人利用
cache:
type: redis # 配置使用 Redis 进行缓存
# cache-names: # 如果没配名字,就按照系统中用到的缓存进行起名
redis:
time-to-live: 360000 # 设置过期时间,单位是 ms
# key-prefix: CACHE_ # key 前缀,推荐不指定,这样分区名(value)默认就是缓存的前缀
use-key-prefix: true # 是否使用写入 Redis 前缀
cache-null-values: true # 是否允许缓存空值,可用于防止缓存穿透