Springboot整合Springcache

一、前言

1、SpringCache是Spring提供的一个缓存框架,在Spring3.1版本开始支持将缓存添加到现有的spring应用程序中,在4.1开始,缓存已支持JSR-107注释和更多自定义的选项。

2、Spring Cache利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了,做到了对代码侵入性做小。

3、由于市面上的缓存工具实在太多,SpringCache框架还提供了CacheManager接口,可以实现降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine、Guava Cache、Ehcache。

二、SpringCache深入

一、SpringCache大致原理

在这里插入图片描述

在SpringCache官网中,有一个缓存抽象的概念,其核心就是将缓存应用于Java方法中,从而减少基于缓存中可用信息的执行次数。换句话来说。就是每次调用目标方法前,SpringCache都会先检查该方法是否正对给定参数执行,如果已经执行过,就直接返回缓存的结果。(通俗的讲,就是查看缓存里面是否有对应的数据,如果有就返回缓存的数据),而无需执行实际方法、如果该方法上位执行。则执行该方法(缓存中没有对应的数据就执行方法获取对应数据,并进行缓存),并缓存结果并返回给用户。这样就不用多次去执行数据库操作,减少cpu和io的消耗。

二、SpringCache为我们提供了两个接口:

1、org.springframework.cache.Cache:

​ Cache接口为缓存的组件规范定义,包含缓存的各种操作集合

2、org.springframework.cache.CacheManager:

CacheManager接口下Spring提供了各种xxxCache的实现;如RedisCache、EhCacheCache、ConcurrentMapCache等;

三、SpringCache概念

1、Cache接口: 缓存接口,定义缓存操作。实现有 如RedisCache、EhCacheCache、ConcurrentMapCache等

2、cacheResolver: 指定获取解析器

3、CacheManager: 缓存管理器,管理各种缓存(Cache)组件;如:RedisCacheManager,使用redis作为缓存。指定缓存管理器

4、@Cacheable:在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。

5、@CacheEvict: 将一条或多条数据从缓存中删除。

6、@CachePut: 将方法的返回值放到缓存中

7、@EnableCaching: 开启缓存注解功能

8、@Caching: 组合多个缓存注解;

9、@CacheConfig: 统一配置@Cacheable中的value值

四、SpringCache注解中公共的属性

1、cacheNames:每个注解中都有自己的缓存名字。该名字的缓存与方法相关联,每次调用时,都会检查缓存以查看是否有对应cacheNames名字的数据,避免重复调用方法。名字可以可以有多个,在这种情况下,在执行方法之前,如果至少命中一个缓存,则返回相关联的值。( Springcache提供两个参数来指定缓存名:value、cacheNames,二者选其一即可,每一个需要缓存的数据都需要指定要放到哪个名字的缓存,缓存的分区,按照业务类型分 )

1
2
3
4
5
6
@Cacheable(cacheNames = "tets")


// ex
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

2KeyGenerator:key生成器

缓存的本质是key-value存储模式,每一次方法的调用都需要生成相应的Key, 才能操作缓存。

通常情况下,@Cacheable有一个属性key可以直接定义缓存key,开发者可以使用SpEL语言定义key值。若没有指定属性key,缓存抽象提供了 KeyGenerator来生成key ,具体源码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleKeyGenerator implements KeyGenerator {
public SimpleKeyGenerator() {
}

public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}

public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
} else {
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}

return new SimpleKey(params);
}
}
}

由此可看出

  • 如果没有参数,则直接返回SimpleKey.EMPTY
  • 如果只有一个参数,则直接返回该参数;
  • 若有多个参数,则返回包含多个参数的SimpleKey对象。

当然Spring Cache也考虑到需要自定义Key生成方式,需要我们实现org.springframework.cache.interceptor.KeyGenerator 接口

默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现。

3、key:缓存的key,如果是redis,则相当于redis的key

可以为空,如果需要可以使用spel表达式进行表写。如果为空,则缺省默认使用key表达式生成器进行生成。默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现。key的生成器。key/keyGenerator二选一使用

4、condition:缓存的条件,对入参进行判断

可以为空,如果需要指定,需要使用SPEL表达式,返回true/false,只有返回true的时候才会对数据源进行缓存/清除缓存。在方法调用之前或之后都能进行判断。

condition=false时,不读取缓存,直接执行方法体,并返回结果,同时返回结果也不放入缓存。

condition=true时,读取缓存,有缓存则直接返回。无则执行方法体,同时返回结果放入缓存(如果配置了result,且要求不为空,则不会缓存结果)。

注意:

condition 属性使用的SpEL语言只有#root和获取参数类的SpEL表达式,不能使用返回结果的#result 。所以 condition = "#result != null" 会导致所有对象都不进入缓存,每次操作都要经过数据库。

五、Spel表达式

一、spel语法

具体语法可以参考此篇博客:SpEL表达式总结 - 简书

img

二、SpringCache也提供了root对象,具体功能使用如下。

img

三、使用spel表达式栗子

1、使用参数作为key:使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Cacheable(value="users", key="#id")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#p0")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#user.id")
public User find(User user) {
returnnull;
}
@Cacheable(value="users", key="#p0.id")
public User find(User user) {
returnnull;
}

2、当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

1
2
3
4
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
returnnull;
}

如果要调用类里面的方法:

1
2
3
4
5
6
7
8
9
10
@Cacheable(value={"TeacherAnalysis_public_chart"}, 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 "";
}

3、最好使用所有参数作为key,当然,也分情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Cacheable(cacheNames = "c2",key = "#id")
public User getUserById(Long id,String username){
User user = new User();
user.setId(id);
return user;
}
@Test
void testGetUserById() {
User u1 = userService.getUserById(98L, "dong");
User u2 = userService.getUserById(98L, "lisi");
}
/**
以参数id作为key会出现逻辑错误,当调用第一次getUserById方法时,存入key为id,值为dong,当调用第二次getUserById方法时,因为已经存入缓存id,所以不会进入第二次getUserById方法,所以lisi不能进入缓存
**/

4、自定义key生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

//将myKeyGenerate注入
@Cacheable(cacheNames = "test",keyGenerator = "myKeyGenerate")
public User getUserById(Long id,String username){
User user = new User();
user.setId(id);
user.setUsername(username);
return user;
}

五、各个注解详解

在四当中,已经把各个注解的公共属性抽了出来,这里只做一些注解的特有属性,当然,可能某些属性也是公有的。

一、@Cacheable:在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。

1、unless:条件符合则不缓存,是对出参进行判断

unless属性可以使用#result表达式。效果: 缓存如果有符合要求的缓存数据则直接返回,没有则去数据库查数据,查到了就返回并且存在缓存一份,没查到就不存缓存。

condition 不指定相当于 true,unless 不指定相当于 false

    当 condition = false,一定不会缓存;

    当 condition = true,且 unless = true,不缓存;

    当 condition = true,且 unless = false,缓存;

2、sync:是否使用异步,默认是false.

​ 在一个多线程的环境中,某些操作可能被相同的参数并发地调用,同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待。当值为true,相当于同步可以有效的避免缓存穿透的问题。

1
2
3
4
5
6
@Cacheable(value="user_cache",key="#userId", unless="#result == null")
public User getUserById(Long userId) {
User user = userMapper.getUserById(userId);
return user;
}

二、@CachePut:缓存更新

与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

他所具有的属性与@Cacheable相同就不多描述。

1
2
3
4
5
6
7
8
@CachePut(value = "user_cache", key="#user.id", unless = "#result != null")
public User updateUser(User user) {
userMapper.updateUser(user);
return user;
}
/**
当调用updateUser方法时,每次方法都会被执行,但是因为unless属性每次都是true,所以并没有将结果缓存。当去掉unless属性,则结果会被缓存。
**/

三、@CacheEvict:清空缓存

注解的方法在调用时会从缓存中移除已存储的数据。

1
2
3
4
@CacheEvict(value = "user_cache", key = "#id")
public void deleteUserById(Long id) {
userMapper.deleteUserById(id);
}

1、allEntries:是否清空左右缓存。默认为false

当指定了allEntries为true时,Spring Cache将忽略指定的key

2、beforeInvocation:是否在方法执行前就清空,默认为 false

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

四、@Caching:可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解

1、其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

1
2
3
4
5
6
7
8
9
10
11
12
@Caching(
cacheable = {@Cacheable(value = "stu",key = "#userName")},
put = {@CachePut(value = "stu", key = "#result.id"),
@CachePut(value = "stu", key = "#result.age")
}
)
public Student getStuByStr(String userName) {
StudentExample studentExample = new StudentExample();
studentExample.createCriteria().andUserNameEqualTo(userName);
List<Student> students = studentMapper.selectByExample(studentExample);
return Optional.ofNullable(students).orElse(null).get(0);
}

五、@CacheConfig 抽取缓存的公共配置

我们每个缓存注解中 都指定 了value = “stu” / cacheNames=”stu” ,可以抽离出来,在整个类上添加

@CacheConfig(cacheNames = “stu”),之后每个方法中都默认使用 cacheNames = “stu”

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
41
42
43
44
45
46
47
48
@CacheConfig(cacheNames = "stu")
@Service
public class StudentServiceImpl implements StudentService {

@Resource
private StudentMapper studentMapper;

@CachePut(key = "#result.id")
@Override
public Student updateStu(Student student){
System.out.println(student);
studentMapper.updateByPrimaryKey(student);
return student;
}
/**
* Cacheable
* @param id
* @return
*
* key = "#root.methodName+'['+#id+']'"
*/
@Cacheable(key = "#id")
@Override
public Student getStu(Integer id) {
Student student = studentMapper.selectByPrimaryKey(id);
System.out.println(student);
return student;
}

@CacheEvict(allEntries = true, beforeInvocation = true)
public void delSut(Integer id) {
System.out.println(id);
studentMapper.deleteByPrimaryKey(id);
}

@Caching(
cacheable = {@Cacheable(key = "#userName")},
put = {@CachePut(key = "#result.id"),
@CachePut(key = "#result.age")
}
)
public Student getStuByStr(String userName) {
StudentExample studentExample = new StudentExample();
studentExample.createCriteria().andUserNameEqualTo(userName);
List<Student> students = studentMapper.selectByExample(studentExample);
return Optional.ofNullable(students).orElse(null).get(0);
}
}

三、SpringCache使用步骤:

一、Springboot整合redis与cache

1、对于使用Springboot进行整合redis与cache,我们只需要引入

pom.xml

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>

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

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.properties

1
2
3
4
5
6
7
8
9


server.port=9001


spring.redis.host=192.168.55.101
spring.redis.port=6379
spring.redis.password=xxxx
spring.cache.type=redis

RedissonConfig

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
package com.cactus.springcache.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class RedissonConfig {

@Value("${spring.redis.host:127.0.0.1}")
private String redisIp;
@Value("${spring.redis.port:6379}")
private String redisPort;
@Value("${spring.redis.password:password}")
private String redisPassword;
@Bean
public RedissonClient redissonClient() throws IOException {
// 默认连接地址 127.0.0.1:6379
// RedissonClient redisson = Redisson.create();

Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisIp + ":" + redisPort);
config.useSingleServer().setDatabase(1);
config.useSingleServer().setPassword(redisPassword);
return Redisson.create(config);
}
}


CacheConfig

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
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.cactus.springcache.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
* redis cache 配置
* 继承缓存配置
*/
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

//过期时间 可有可无 如果不写就是永久保存 看需求 这里是缓存一个小时
//建议是永久保存
config = config.entryTtl(Duration.ofHours(1));
//序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//反序列化
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.prefixCacheNameWith(redisProperties.getKeyPrefix());;
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;

}
}

BaseResult

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
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
71
72
73
74
75
76
77
78
package com.cactus.springcache.common;

import lombok.Data;

@Data
public class BaseResult {
private int code;
private String msg;
private Object data;

public static BaseResult success() {
BaseResult result = new BaseResult();
result.code = 0;
result.msg= "success";
return result;
}
public static BaseResult success(String msg) {
BaseResult result = new BaseResult();
result.code = 0;
result.msg= msg;
return result;
}
public static BaseResult success(Object data) {
BaseResult result = new BaseResult();
result.code = 0;
result.msg= "success";
result.data = data;
return result;
}
public static BaseResult success(String msg,Object data) {
BaseResult result = new BaseResult();
result.code = 0;
result.msg= msg;
result.data = data;
return result;
}
public static BaseResult success(int code,String msg,Object data) {
BaseResult result = new BaseResult();
result.code = code;
result.msg= msg;
result.data = data;
return result;
}
public static BaseResult error() {
BaseResult result = new BaseResult();
result.code = -1;
result.msg= "error";
return result;
}
public static BaseResult error(String msg) {
BaseResult result = new BaseResult();
result.code = -1;
result.msg= msg;
return result;
}
public static BaseResult error(Object data) {
BaseResult result = new BaseResult();
result.code = -1;
result.msg= "error";
result.data = data;
return result;
}
public static BaseResult error(String msg,Object data) {
BaseResult result = new BaseResult();
result.code = -1;
result.msg= msg;
result.data = data;
return result;
}
public static BaseResult error(int code,String msg,Object data) {
BaseResult result = new BaseResult();
result.code = code;
result.msg= msg;
result.data = data;
return result;
}
}

User

1
2
3
4
5
6
7
8
9
10
11
package com.cactus.springcache.entity;

import lombok.Data;

@Data
public class User {
private String username;
private String id;
private Integer age;
}

TestController

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
41
42
43
44
45
46
47
48
49
package com.cactus.springcache.controller;

import com.cactus.springcache.common.BaseResult;
import com.cactus.springcache.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/test")
public class TestController {


@RequestMapping("/user")
@Cacheable(value = "cache_user",key = "'userTestCache'")
public BaseResult testCache() {
List<User> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setUsername("张三" + i +1);
user.setId(UUID.randomUUID().toString());
user.setAge(18 +i);
list.add(user);
}
return BaseResult.success(list);
}

@RequestMapping("/user/{id}")
@Cacheable(value = "cache_user", key = "#id")
public BaseResult testCache2(@PathVariable("id") String id) {
User user = new User();
user.setUsername("张三");
user.setId(id);
user.setAge(18);
return BaseResult.success(user);
}
@CacheEvict(allEntries = true,cacheNames = "cache_user")
@RequestMapping("/clean")
public BaseResult clear() {
return BaseResult.success();
}
}

四、测试

1
GET http://localhost:9001/test/user

写入缓存

image-20230911104624380

1
GET http://localhost:9001/test/user/1

单个写入

image-20230911104718533

1
GET http://localhost:9001/test/clean

全部清除

image-20230911104810552


Springboot整合Springcache
https://cai-qichang.github.io/2023/09/11/Springboot整合Springcache/
作者
caiqichang
发布于
2023年9月11日
许可协议
BY-蔡奇倡