SpringBoot 缓存篇
程序员文章站
2022-07-06 16:31:58
一、预备知识1.1 JSR107缓存规范Java Caching 5个核心接口:CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。Cache:是一个类似Map的数据结构并临时存储...
一、预备知识
1.1 JSR107缓存规范
Java Caching 5个核心接口:
- CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
- CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
- Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
- Entry:是一个存储在Cache中的key-value对。
- Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
1.2 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 |
注意:
- 当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。 如
@Cacheable(key = "targetClass + methodName +#p0")
- 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。 如:
@Cacheable(value="users", key="#id")
@Cacheable(value="users", key="#p0")
SpEL提供了多种运算符
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&, |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],^ […],$[…] |
二、Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术,并支持使用JCache(JSR-107)
注解简化我们开发。
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。
- Cache接口下Spring提供了各种xxxCache的实现,如RedisCache、EhCacheCache 、ConcurrentMapCache等。
- 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
三、几个重要概念&缓存注解
名称 | 解释 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。与@Cacheable区别在于是否每次都调用方法,常用于更新 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
@Cacheable/@CachePut/@CacheEvict 主要的参数
名称 | 解释 | 实例 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
allEntries (@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation (@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
unless (@CachePut) (@Cacheable) | 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 | @Cacheable(value=”testcache”,unless=”#result == null”) |
四、基本环境搭建
- 环境:Spring Boot 2.3.1
- IDE:IDEA
4.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
4.2 创建数据库&表
sql文件:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
4.3 配置数据源信息
application.yml
spring:
datasource:
# 数据源基本配置
username: root
password: "00000000"
url: jdbc:mysql://192.168.168.128/springboot_cache?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
redis:
host: 192.168.168.128
password: "00000000"
mybatis:
configuration:
# 开启驼峰命名匹配规则
map-underscore-to-camel-case: true
# 配置Sql语句日志
logging:
level:
com:
example:
cache:
mapper: debug
五、开始使用
5.1 在启动类注解@EnableCaching开启缓存
@EnableCaching // 开启基于注解的缓存
@MapperScan("com.example.cache.mapper") // 指定要扫描的mapper接口所在的包
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
5.2 缓存 @Cacheable
- @Cacheable 注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。
- 此处的 value 是必需的,它指定了你的缓存存放在哪块命名空间。
- 此处的key是使用的spEL表达式。这里有一个小坑,如果你把 methodName 换成 method 运行会报错,观察它们的返回类型,原因在于 methodName 是 String 而 methoh 是 Method 。
- 此处的 Employee 实体类一定要实现序列化Serializable,否则会报 java.io.NotSerializableException 异常。
// 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@Cacheable(value = "emp", key = "#id")
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
return employeeMapper.getEmpById(id);
}
测试:
多次查询同一员工数据只执行一次数据库查询。
5.3 自定义缓存配置生成Key
配置类
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return (o, method, objects) -> method.getName() + Arrays.asList(objects).toString();
}
}
修改注解
@Cacheable(value = "emp", keyGenerator = "myKeyGenerator")
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
return employeeMapper.getEmpById(id);
}
5.4 更新 @CachePut
- @CachePut 注解的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
- @CachePut 注解每次都会触发真实方法的调用。
- 需要注意的是该注解的 value 和 key 必须与要更新的缓存相同,也就是与 @Cacheable 相同。
- 运行时机:先调用方法,后将目标方法的结果缓存起来。
// 保证方法被调用,同时结果被缓存
@CachePut(value = "emp", key = "#employee.id")
public Employee updateEmp(Employee employee) {
System.out.println("更新了员工:" + employee.getLastName());
employeeMapper.updateEmp(employee);
return employee;
}
测试:
更新员工信息后缓存随之修改。查询仍然从缓存中获取。
5.5 清除 @CacheEvict
- @CachEvict 的作用主要针对方法配置,能够根据一定的条件对缓存进行清空。
属性 | 解释 | 示例 |
---|---|---|
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
- 清除一条缓存,key为要清空的数据
@CacheEvict(value = "emp", key = "#id")
public void deleteEmp(Integer id) {
System.out.println("删除了员工:" + id);
employeeMapper.deleteEmpById(id);
}
- 方法调用后清空所有缓存
@CacheEvict(value = "accountCache", allEntries = true)
public void deleteAll() {
System.out.println("删除了所有员工");
employeeMapper.deleteAll();
}
- 方法调用前清空所有缓存
@CacheEvict(value = "accountCache", beforeInvocation = true)
public void deleteAll() {
System.out.println("删除了所有员工");
employeeMapper.deleteAll();
}
5.6 组合 @Caching
- 有时候我们可能组合多个 Cache 注解使用,此时就需要 @Caching 组合多个注解标签了。
@Caching(
cacheable = {
@Cacheable(value = "emp", key = "#lastName")
},
put = {
@CachePut(value = "emp", key = "#result.id"),
@CachePut(value = "emp", key = "#result.email")
},
evict = {
@CacheEvict(value = "emp", key = "#lastName")
}
)
public Employee getEmpByLastName(String lastName) {
return employeeMapper.getEmpByLastName(lastName);
}
5.7 配置 @CacheConfig
- 当我们需要缓存的地方越来越多,你可以使用 @CacheConfig 注解来统一指定value的值,这时可省略 value ,如果你在你的方法依旧写上了 value ,那么依然以方法的value值为准。
@CacheConfig(cacheNames = "emp") // 抽取缓存的公共配置
@Service
public class EmployeeService {
...
}
六、整合Redis实现缓存
6.1 部署Redis
- 拉取镜像
docker pull redis
- 创建Redis容器并设置密码
docker run --name redis -p 6379:6379 redis --requirepass 123456
6.2 导入依赖
导入此依赖时,不再需要spring-boot-starter-cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
6.3 测试保存数据
@SpringBootTest
class CacheApplicationTests {
@Autowired
StringRedisTemplate stringRedisTemplate; // 操作k-v都是字符串的
@Autowired
RedisTemplate redisTemplate; // 操作k-v都是对象的
@Test
void Test02() {
// redis中保存数据
stringRedisTemplate.opsForValue().append("msg", "Hello");
// redis中获取数据
String msg = stringRedisTemplate.opsForValue().get("msg");
System.out.println(msg);
}
@Test
void Test03() {
// redis中保存对象
Employee empById = employeeMapper.getEmpById(1);
// 默认使用jdk序列化机制保存对象
redisTemplate.opsForValue().set("emp-01", empById);
}
}
6.4 自定义序列化机制
将数据以 Json 的方式保存
@Configuration
public class MyRedisConfig {
// 自定义Employee序列化机制
@Bean
public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(serializer);
return template;
}
// 自定义Department序列化机制
@Bean
public RedisTemplate<Object, Department> deptRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Department> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Department> serializer = new Jackson2JsonRedisSerializer<Department>(Department.class);
template.setDefaultSerializer(serializer);
return template;
}
}
测试
@SpringBootTest
class CacheApplicationTests {
@Resource
EmployeeMapper employeeMapper;
@Resource
DepartmentMapper departmentMapper;
@Autowired
StringRedisTemplate stringRedisTemplate; // 操作k-v都是字符串的
@Autowired
RedisTemplate redisTemplate; // 操作k-v都是对象的
@Autowired
RedisTemplate<Object, Employee> empRedisTemplate;
@Autowired
RedisTemplate<Object, Department> deptRedisTemplate;
@Test
void Test03() {
// redis中保存对象
Employee empById = employeeMapper.getEmpById(1);
// 默认使用jdk序列化机制保存对象
// redisTemplate.opsForValue().set("emp-01", empById);
empRedisTemplate.opsForValue().set("emp-01", empById);
}
@Test
void Test04() {
// redis中保存对象
Department deptById = departmentMapper.getDeptById(1);
// 使用Json保存对象
deptRedisTemplate.opsForValue().set("dept-01", deptById);
}
}
6.5 自定义CacheManager
@Configuration
public class MyRedisConfig {
// 自定义CacheManager
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
}
测试
本文地址:https://blog.csdn.net/weixin_41105242/article/details/107187153