Spring - Cache
# 前言
我们一定听说过 缓存无敌 的话,特别是在大型互联网公司,查多写少 的场景屡见不鲜。网络上查到的很多诸如系统吞吐量提升 50%、接口耗时降低 80%、一个分钟级别的程序优化到毫秒级别等,多多少少和缓存有关。
举个例子:在我们程序中,很多配置数据(例如一个商品信息、一个白名单、一个第三方客户的回调接口),这些数据存在我们的DB上,数据量比较少,但是程序访问很频繁,这种情况下,将数据放一份到我们的内存缓存中将大大提升我们系统的访问效率,因为减少了数据库访问,有可能减少了数据库建连时间、网络数据传输时间、数据库磁盘寻址时间。
总的来说,下面这些场景都可以考虑使用缓存优化性能:
- 查数据库
- 读取文件
- 网络访问,特别是调用第三方服务查询接口
SpringCache 是 Spring 提供的一个缓存框架,在 Spring3.1 版本开始支持将缓存添加到现有的 Spring 应用程序中,在 4.1 开始,缓存已支持 JSR-107 注释和更多自定义的选项。
Spring Cache 利用了 AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了,做到了对代码侵入性做小。
由于市面上的缓存工具实在太多,SpringCache 框架还提供了 CacheManager
接口,可以实现降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如 Caffeine、Guava Cache、Ehcache。
# 深入
在 SpringCache 官网中,有一个缓存抽象的概念,其核心就是将缓存应用于 Java 方法中,从而减少基于缓存中可用信息的执行次数。换句话来说。就是每次调用目标方法前,SpringCache 都会先检查该方法是否正对给定参数执行,如果已经执行过,就直接返回缓存的结果。(通俗的讲,就是查看缓存里面是否有对应的数据,如果有就返回缓存的数据),而无需执行实际方法、如果该方法上位执行。则执行该方法(缓存中没有对应的数据就执行方法获取对应数据,并进行缓存),并缓存结果并返回给用户。这样就不用多次去执行数据库操作,减少 CPU 和 IO 的消耗。
# 两个接口
SpringCache 为我们提供了两个接口:
org.springframework.cache.Cache
:Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合。
org.springframework.cache.CacheManager
:CacheManager 接口下 Spring 提供了各种 xxxCache
的实现;如RedisCache、EhCacheCache、ConcurrentMapCache 等。
# SpringCache 概念
Cache
接口:缓存接口,定义缓存操作。实现有 如 RedisCache、EhCacheCache、ConcurrentMapCache 等CacheResolver
:指定获取解析器CacheManager
:缓存管理器,管理各种缓存(Cache)组件;如:RedisCacheManager,使用 Redis 作为缓存。指定缓存管理器@Cacheable
:在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。@CacheEvict
:将一条或多条数据从缓存中删除@CachePut
:将方法的返回值放到缓存中@EnableCaching
:开启缓存注解功能,在 SpringBoot 启动类上使用@Caching
:组合多个缓存注解@CacheConfig
:统一配置@Cacheable
中的 value 值
在开发中,常用的是 @Cacheable
、@CacheEvict
、@CachePut
三个注解,分别:
@Cacheable
查询数据库后,将得到的数据进行缓存@CacheEvict
更新数据库的数据时,把更新的旧数据从缓存中删除@CachePut
更新数据库的数据时,顺便在缓存里也进行更新
# 注解属性
开发常用的几个注解,介绍内部的属性。
# cacheNames
每个注解中都有自己的缓存名字。该名字的缓存与方法相关联,每次调用时,都会检查缓存以查看是否有对应 cacheNames
名字的数据,避免重复调用方法。名字可以可以有多个,在这种情况下,在执行方法之前,如果至少命中一个缓存,则返回相关联的值。(Springcache 提供两个参数来指定缓存名:value、cacheNames,二者选其一即可,因为功能一样,每一个需要缓存的数据都需要指定要放到哪个名字的缓存,缓存的分区,按照业务类型分)
@Cacheable(cacheNames = "uuid)
public String getUuid() {
return UUID.randomUUID().toString().replace("-", "");
}
2
3
4
# key
缓存的 key,如果是 Redis,则相当于 Redis 的 key。
可以为空,如果需要可以使用 SpEL
表达式进行表写。如果为空,则缺省默认使用 key 表达式生成器进行生成。默认的 key 生成器要求参数具有有效的 hashCode()
和 equals()
方法实现。key 的生成器。key / keyGenerator
二选一使用。
# KeyGenerator
这是 key 生成器,缓存的本质是 key-value
存储模式,每一次方法的调用都需要生成相应的 Key, 才能操作缓存。
通常情况下,@Cacheable
有一个属性 key 可以直接定义缓存 key,开发者可以使用 SpEL
语言定义 key 值。若没有指定属性 key,缓存抽象提供了 KeyGenerator 来生成 key
具体源码如下:
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可看出
- 如果没有参数,则直接返回
SimpleKey.EMPTY
- 如果只有一个参数,则直接返回该参数
- 若有多个参数,则返回包含多个参数的
SimpleKey
对象
当然 Spring Cache 也考虑到需要自定义 Key 生成方式,需要我们实现 org.springframework.cache.interceptor.KeyGenerator
接口。
默认的 key 生成器要求参数具有有效的 hashCode()
和 equals()
方法实现。
# 自定义 key 生成器
@Component
public class MyKeyGenerate implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
String s = target.toString()+":"+method.getName()+":"+ Arrays.toString(params);
return s;
}
}
@Cacheable(cacheNames = "test", keyGenerator = "myKeyGenerate")
public User getUserById(Long id,String username){
User user = new User();
user.setId(id);
user.setUsername(username);
return user;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# condition
缓存的条件,对入参进行判断,符合条件的缓存,不符合的不缓存。
可以为空,如果需要指定,需要使用 SpEL 表达式,返回 true/false
,只有返回 true 的时候才会对数据源进行缓存/清除缓存。在方法调用之前或之后都能进行判断。
condition=false
时,不读取缓存,直接执行方法体,并返回结果,同时返回结果也不放入缓存。
condition=true
时,读取缓存,有缓存则直接返回。无则执行方法体,同时返回结果放入缓存(如果配置了 result,且要求不为空,则不会缓存结果)。
注意:
condition 属性使用的 SpEL 语言只有 #root
和获取参数类的 SpEL 表达式,不能使用返回结果的 #result
。所以 condition = "#result != null"
会导致所有对象都不进入缓存,每次操作都要经过数据库。
@Cacheable(value = "uuid", key = "#key", condition = "#root.args[0] != null")
public String getUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
2
3
4
# unless
和 condition 相反,符合条件的不缓存,不符合的缓存。
既可以使用 #root
,也可以使用 #result
表达式。
效果: 缓存如果有符合要求的缓存数据则直接返回,没有则去数据库查数据,查到了就返回并且存在缓存一份,没查到就不存缓存。
@Cacheable(value = "uuid", key = "#key", unless = "#result != null")
public String getUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
2
3
4
condition 不指定相当于 true,unless 不指定相当于 false
当
condition = false
,一定不会缓存当
condition = true
,且unless = true
,不缓存当
condition = true
,且unless = false
,缓存
# Sync
是否使用异步,默认是 false
。
在一个多线程的环境中,某些操作可能被相同的参数并发地调用,同一个 value 值可能被多次计算(或多次访问数据库),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync
参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待。当值为 true,相当于同步可以有效的避免缓存穿透的问题。
# allEntries
@CacheEvict
特有的属性:是否清空左右缓存。默认为 false
,当指定了allEntries为true时,Spring Cache将忽略指定的key
# beforeInvocation
@CacheEvict
特有的属性:是否在方法执行前就清空,默认为 false
。
清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用 beforeInvocation
可以改变触发清除操作的时间,当我们指定该属性值为 true
时,Spring 会在调用该方法之前清除缓存中的指定元素。
# Spel 表达式
SpEL(Spring Expression Language),即 Spring 表达式语言,是比 JSP 的 EL 更强大的一种表达式语言。为什么要总结 SpEL,因为它可以在运行时查询和操作数据,尤其是数组列表型数据,因此可以缩减代码量,优化代码结构。
具体使用文档后续再出。
SpringCache 也提供了 root 对象,具体功能使用如下:
名字 | 位置 | 描述 | 例子 |
---|---|---|---|
methodName | 根对象 | 要调用的方法名称 | #root.methodName |
method | 根对象 | 正在调用的方法 | #root.method.name |
target | 根对象 | 正在调用的目标对象 | #root.target |
targetClass | 根对象 | 要调用的目标类 | #root.targetClass |
args | 根对象 | 用于调用目标的参数(数组) | #root.args[0] |
caches | 根对象 | 对其执行当前方法的缓存集合 | #root.caches[0].name |
result | 返回值 | 方法的返回值(要缓存的值) | #result |
# SpEL 例子
使用参数作为 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当我们要使用 root 对象的属性作为 key 时我们也可以将 #root
省略,因为 Spring 默认使用的就是 root 对象的属性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
returnnull;
}
2
3
4
要调用类里面的其他方法:
@Cacheable(value={"chartList"}, key="#root.target.getDictTableName() + '_' + #root.target.getFieldName()")
public List<Map<String, Object>> getChartList(Map<String, Object> paramMap) {
}
public String getDictTableName(){
return "";
}
public String getFieldName(){
return "";
}
2
3
4
5
6
7
8
9
10
11
# @CacheConfig
这是抽取缓存的公共配置,因为常用的注解有很多频繁使用的属性,所以我们不希望每个注解都写一遍这些常用的属性,如:
@Cacheable(cacheNames = "uuid", key = "#key")
public String getUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
@CacheEvict(cacheNames = "uuid", key = "#name")
public String getNewUuid(String name) {
return UUID.randomUUID().toString().replace("-", "");
}
@CachePut(cacheNames = "uuid", key = "#age")
public String updateUuid(String age) {
return UUID.randomUUID().toString().replace("-", "");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
所以可以使用 @CacheConfig
注解作用在类上:
@CacheConfig(cacheNames = "uuid")
@Service
public class Test {
@Cacheable( key = "#key")
public String getUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
@CacheEvict(key = "#key")
public String getNewUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
@CachePut(key = "#key")
public String updateUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
@Caching(
cacheable = {@Cacheable(key = "#userName")},
put = {@CachePut(key = "#result.id"),
@CachePut(key = "#result.age")
}
)
public Student getStuByStr(String userName) {
return UUID.randomUUID().toString().replace("-", "");
}
}
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
# 整合 Redis
说到缓存,我们会优先考虑到 Redis,Spring Cache 也支持 Redis,所以我们以 Redis 作为例子。
引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
因为都是 SpringBoot 的 Started,所以版本就会自动跟随 SpringBoot 的版本。
配置 Redis:
@Configuration
public class RestTemplateConfig {
private final RedisConnectionFactory redisConnectionFactory;
public RestTemplateConfig(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
/*
* 设置 value 的序列化规则和 key 的序列化规则
* RedisTemplate 默认序列化使用的 jdkSerializeable, 存储到 Redis 会变成二进制字节码,有风险!
* 所以官网建议转成其他序列化
*/
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 支持 Spring 事务
* @return redisTemplate
*/
@Bean
public RedisTemplate<Object, Object> tranRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
/*
* 置 value 的序列化规则和 key 的序列化规则
* RedisTemplate 默认序列化使用的 jdkSerializeable, 存储到 Redis 会变成二进制字节码,有风险!
* 所以官网建议转成其他序列化
*/
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
// 支持 spring 事务,如 @Transactional 注解
redisTemplate.setEnableTransactionSupport(true);
return redisTemplate;
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Controller
@RestController
public class RedisController {
private final RedisService redisService;
public RedisController(RedisService redisService) {
this.redisService = redisService;
}
@GetMapping("/getUUID")
public Response<String> getUuid() {
return HttpResult.okMessage(redisService.getUuid("a"));
}
@GetMapping("/getNewUuid")
public Response<String> getNewUuid() {
return HttpResult.okMessage(redisService.getNewUuid());
}
@GetMapping("/updateUuid")
public Response<String> updateUuid() {
return HttpResult.okMessage(redisService.updateUuid());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Service
@Service
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
private final static String CACHE_NAMES = "uuid";
private final static String CACHE_KEY = "test";
public RedisService(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String setRredisString(String key, String value) {
return "存储成功";
}
@Cacheable(cacheNames = CACHE_NAMES, key = "#key")
public String getUuid(String key) {
return UUID.randomUUID().toString().replace("-", "");
}
@CacheEvict(cacheNames = CACHE_NAMES, key = "'test'")
public String getNewUuid() {
return UUID.randomUUID().toString().replace("-", "");
}
@CachePut(cacheNames = CACHE_NAMES, key = "'test'")
public String updateUuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
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
当我们重复去调用 Controller 的 /getUUID
接口时,并去 Redis 数据库查看,第二次以上的调用,数据都来自 Redis。
因为第一次生成随机 UUID 后,Spring Cache 会将 UUID 存入 Redis,下次请求的时候,直接去 Redis 读取。
存在 Redis 的 key 并不是 @Cacheable
的 key,而是 CACHE_NAMES:key
,即如果传入的参数 key 为 kbt,则 Redis 存入的 key 就是 uuid:kbt
,value 则是 UUID。
当调用 /getNewUuid
时,发现返回了一个新的 UUID,同时 Redis 存储的 UUID 被删除。
当调用 /updateUuid
时,发现返回了一个新的 UUID,同时 Redis 存储的 UUID 被替换为新的 UUID。