前言

缓存相信各位都不陌生,作为开发中非常重要的一个组件。是解决高并发与数据库压力的第一方案,目前使用最多的是 redis 缓存中间件。但对于一些 CRUD 的缓存,还是有一些人使用的是 redisTemplate。开发中经常重复造车轮,以至于没有太多的时间去理解业务和逻辑。而 SpringCache 就出现了。只需要一行注解,几个配置,缓存的任务就完成了。我们只需要专心写逻辑就可以。使用也十分简单。本次就 SpringCache 结合当下最常用的 redis 来实践理解一下

SpringCache 的注解

首先是三个常用注解

@Cacheable:获取缓存,如果获取不到缓存则执行本方法 将方法的返回值缓存到redis

@CachePut:更新缓存,无论缓存是否存在,都会执行本方法,并且更新到redis

@CacheEvict:删除缓存,缓存存在则删除,不存在自然不会删

下面来看看 Cacheable 注解中的值

image-20221126102824083

cacheNames:可以理解为缓存容器的名字,也就是缓存要放进的地方

key:理解为redis的key就可以 表示缓存名字

keyGenerator:缓存key的生成策略 后面会详细说明

cacheManager:指定缓存容器,要从哪个缓存容器获取缓存

cacheResolver:管理keyManager的东西

condition:el表达式,写入一个条件,方法执行前判断, 为true则存入缓存 false不会存入

unless:el表达式,写入一个条件 方法执行完成后判断,为true不会写入缓存 false会存入(可用于判断返回值中的内容)

sync: 是否开启同步 设置为true可以避免缓存击穿等问题

以上参数中,key 和 keyGenerator 只选其一,cacheResolver 和 cacheResolver 只选其一 (这两个一般也不会去设置)

依赖与配置文件

依赖只需要引入 redis 和 springCache 的依赖就可以

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.5</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.5</version>
</dependency>

yml 这里就更简单,只需要将 type 设置为 redis 就可以了

1
2
3
4
5
server:
port: 8080
spring:
cache:
type: redis

代码实践

@Cacheable 的使用

我们需要在启动类上 @EnableCaching 来开启缓存

写一个方法,逻辑为如果查询到了缓存则获取缓存的内容,没有则执行方法 然后存入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 根据用户id获取用户
* @param userId
* @return
*/
@GetMapping("getUserAble/{id}")
@Cacheable(cacheNames = "user", key = "'-' + #userId", sync = true)
public User getUserByIdAble(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

确保 redis 中目前没有缓存,然后我们执行方法,第一次控制台打印 [进入方法] 表名没有获取到缓存

image-20221126105144795 image-20221126105534612

之后我们继续访问,则不会继续打印,表名我们获取到的是缓存里的内容。其中,最外层的 user 是 cacheNames。key 则为 cacheNames + “::” + 设置的对应 key,也就是图中的 user::-1

自定义 keyGenerator

我们可以会觉得直接在注解上设置 key,会有些死板,这个时候我们可以选择自定义 keyGenerator 来设置 key 的生成策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class UserKeyGenerator {

// 设置这个生成器的bean名字
@Bean("UserKeyGenerator")
public KeyGenerator createUserKeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
System.out.println(target);
System.out.println(method);
System.out.println(params);
Object[] arr = params;
// 使用方法名+ 第一个参数 为key前缀
return method.getName() + "-" + arr[0];
}
};
}
}

target 是我们的类地址,method 为方法,params 则是参数。这里我使用方法名 + 第一个参数作为 key

image-20221126105842379

在方法上设置 keyGenerator 的值选择我们的 key 生成策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 根据用户id获取用户 使用自定义key生成器
* @param userId
* @return
*/
@GetMapping("getUserKeyGenerator/{id}")
@Cacheable(cacheNames = "user", keyGenerator = "UserKeyGenerator", sync = true)
public User getUserByIdKeyGenerator(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

和第一个 key 可以对比以下,key 变成了我们刚刚设置的格式

image-20221126105948684

@CachePut

相对于 Cacheable,put 则要简单得多。不管有没有缓存,都会执行方法,存入 redis,每一次执行都会更新缓存内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 无论是否有缓存 每次都会执行方法内容 写入缓存
* @param userId
* @return
*/
@PutMapping("getUserPut/{id}")
@CachePut(cacheNames = "user", keyGenerator = "UserKeyGenerator")
public User getUserByIdPut(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

可以看到,我们点击几次,就执行方法几次,redis 自然也是一直被更新的状态

image-20221126110301847

@CacheEvict

删除缓存,执行该方法会删除对应的缓存。这里就不演示了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 删除对应的缓存
* @param userId
* @return
*/
@DeleteMapping("getUserEvict/{id}")
@CacheEvict(cacheNames = "user", key = "'-' + #userId")
public User getUserByIdExict(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

下面我们来说说 unless 和 condition 这两个条件

unless

unless 是方法执行完成之后,判断条件是否为 true 为 true 不会存入缓存。可以使用 result 来表示返回值

例如这里设置的是如果查询的用户姓名为王五 则不会存入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 进阶 unless 可使用el表达式 当值为true的时候不会缓存该次记录
* @param userId
* @return
*/
@PostMapping("getUserUnless/{id}")
@Cacheable(cacheNames = "user", key = "'-' + #userId", unless = "#result.name.equals('王五')")
public User getUserByIdUnless(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

我们查询王五和赵六,查询完成后只有赵六被存入了 redis 条件实现

image-20221126110917131

condition

condition 同样也是 el 表达式,不同的是,condition 是在方法执行前判断的。为 true 会存入缓存

例如这里设置的是查询用户 id 大于 2 的才会存入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /**
* 进阶 condition 可使用el表达式 当值为true的时候 会缓存该记录
* @param userId
* @return
*/
@GetMapping("getUserCondition/{id}")
@Cacheable(cacheNames = "user", key = "'-' + #userId", condition = "#userId > 2", sync = true)
public User getUserByIdCondition(@PathVariable("id") Integer userId) {
System.out.println("进入方法");
HashMap<Integer, User> map = new HashMap<>();
map.put(1,new User("张三", 17, "画画"));
map.put(2,new User("李四", 28, "听音乐"));
map.put(3,new User("王五", 26, "旅游"));
map.put(4,new User("赵六", 30, "学习"));
return map.get(userId);
}

我们查询 id 为 1-4 的用户

自然只有 3 和 4 被存入了缓存

image-20221126111130106

看到这里,SpringCache 的基本使用就已经完成了。更高阶的操作例如自定义缓存管理器 cacheManager,可以实现存入时间的设置。