JSR-107规范
Java Caching定义了5个核心接口,分别是CachingProvider(缓存提供者)、CacheManager(缓存管理器)、Cache(缓存)、Entry(缓存键值对)和Expiry(缓存时效)。
CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
Entry:是一个存储在Cache中的key-value对。
Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

一个应用里面可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获取到多个缓存管理器(CacheManager),一个缓存管理器管理着不同的缓存(Cache),缓存中是一个个的缓存键值对(Entry),每个entry都有一个有效期(Expiry)。
Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术。
Cache接口为缓存的组件规范定义,包含缓存的各种操作(增删改查)集合。Cache接口下Spring提供了各种XxxCache的实现,如RedisCache、EhCacheCache、ConcurrentMapCache等。每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
①确定方法需要被缓存以及他们的缓存策略
②从缓存中读取之前缓存存储的数据

几个重要概念&缓存注解:
| 概念/注解 | 作用 |
|---|---|
| Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
| CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
| @Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
| @CacheEvict | 清空缓存 |
| @CachePut | 保证方法被调用,又希望结果被缓存。 |
| @EnableCaching | 开启基于注解的缓存 |
| keyGenerator | 缓存数据时key生成策略 |
| serialize | 缓存数据时value序列化策略 |
@Cacheable标注在方法上,表示该方法的结果需要被缓存起来,缓存的键由keyGenerator的策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之后,在缓存时效内再次调用该方法时将不会调用方法本身而是直接从缓存获取结果(先从缓存中查数据,如果查到了,就直接返回;如果没查到,就执行方法,将查询结果写入缓存)
@CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新(先执行方法,然后将结果缓存)
搭建项目
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.18</version>
</dependency>
数据库中创建employee表用来查询
Employee实体类(getset方法可以用lombok生成,也可以自己写)
public class Employee implements Serializable {
private Integer id;
private String lastName;
private String email;
}
Mapper
@Mapper
public interface EmployeeMapper {
@Select("select * from employee where id=#{id}")
public Employee getEmployeeById(Integer id);
@Update("update employee set lastName=#{lastName},email=#{email} where id=#{id}")
public void updateEmployee(Employee employee);
@Delete("delete from employee where id=#{id}")
public void deleteEmployeeById(Integer id);
}
Service(注意看注释)
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
/**
* 将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;
* CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字;
*
* 运行流程:
* @Cacheable:
* 1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;
* (CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
* 2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;
* key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;
* SimpleKeyGenerator生成key的默认策略;
* 如果没有参数;key=new SimpleKey();
* 如果有一个参数:key=参数的值
* 如果有多个参数:key=new SimpleKey(params);
* 3、没有查到缓存就调用目标方法;
* 4、将目标方法返回的结果,放进缓存中
*
* @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,
* 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据;
* 几个属性:
* cacheNames/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存;
*
* key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值
* 编写SpEL; #id;参数id的值最为key
* #a0 #p0 #root.args[0] a0就是方法中第一个参数,a1就是方法中第二个参数
*
* keyGenerator:key的生成器;可以自己指定key的生成器的组件id
* key/keyGenerator:二选一使用;
* cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
*
* condition:指定符合条件的情况下才缓存;
* ,condition = "#id>0"
* condition = "#a0>1":第一个参数的值>1的时候才进行缓存
*
* unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断
* unless = "#result == null"
* unless = "#a0==2":如果第一个参数的值是2,结果不缓存;
* sync:是否使用异步模式
*
*/
@Cacheable(cacheNames = "emp",key="#id")
public Employee getEmployeeById(Integer id){
System.out.println("执行查询");
Employee employee = employeeMapper.getEmployeeById(id);
return employee;
}
/**
* @CachePut:既调用方法,又更新缓存数据;同步更新缓存
* 修改了数据库的某个数据,同时更新缓存;
* 运行时机:
* 1、先调用目标方法
* 2、将目标方法的结果缓存起来
*/
@CachePut(cacheNames="emp",key="#id")
public Employee updateEmployee(Employee employee) {
System.out.println("执行更新");
employeeMapper.updateEmployee(employee);
return employee;
}
/**
* @CacheEvict:缓存清除
* key:指定要清除的数据
* allEntries = true:指定清除这个缓存中所有的数据
* beforeInvocation = false:缓存的清除是否在方法之前执行
* 默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
* beforeInvocation = true:
* 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
*/
@CacheEvict(cacheNames="emp",key="#id")
public void deleteEmployee(Integer id) {
System.out.println("执行删除");
employeeMapper.deleteEmployeeById(id);
}
}
Controller
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmployeeById(@PathVariable(name="id") Integer id) {
Employee employee=employeeService.getEmployeeById(id);
return employee;
}
@GetMapping("/emp")
public Employee updateEmployee(Employee employee) {
employeeService.updateEmployee(employee);
return employee;
}
@GetMapping("/delemp/{id}")
public String deleteEmployee(@PathVariable(name="id") Integer id) {
employeeService.deleteEmployee(id);
return "success";
}
}
配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
logging:
level:
com.yan.mapper: debug #控制台输出sql语句
启动类上添加注解
@EnableCaching
启动项目
@Cacheable注解
发送get请求 localhost:8080/emp/1
控制台输出

说明连接数据库进行了查询操作
再次发送请求
查询到了数据

控制台没有输出,说明没有执行查询操作,查询的是缓存

@Cacheable会先去查询缓存,如果缓存中没有,就去调用方法,将查询结果写入缓存,如果缓存中有,就直接从缓存中获取数据。具体的介绍在Service代码的注释中
@CachePut注解
发送请求localhost:8080/emp?id=1&lastName=张三&email=48350835@qq.com
更新数据


再次查询id为1的用户,请求 localhost:8080/emp/1

控制台并没有输出新的内容,说明是从缓存中获取到了更新后的数据。

@CachePut先执行方法,然后再把返回结果添加到缓存中
@CacheEvict注解
默认方法执行之后清除缓存,如果方法出现异常就不会清除缓存
发送请求localhost:8080/delemp/1


再次查询数据localhost:8080/emp/1

发现执行了查询操作,说明缓存中的数据已经被删除。
整合Redis做为缓存
Redis安装请查看文章
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
添加配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 192.168.163.128
port: 6379
logging:
level:
com.yan.mapper: debug #控制台输出sql语句
自定义CacheManage,系统的CacheManage默认使用的是java的序列化方式,存在Redis中会乱码,要解决这个问题,就需要自定义一个CacheManage,以json的格式传输数据。
自定义RedisConfig
springboot1.x中,我们配置redis的cacheManager是这种方式:
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间
cacheManager.setDefaultExpiration(10000);
return cacheManager;
} //缓存管理器
springboot2.x中,RedisCacheManager已经没有了RedisCacheManager(RedisOperations redisOperations)这个构造方法。
2.x中的配置方法,包含了设置json序列化和过期时间
@Configuration
@ConfigurationProperties(prefix = "spring.cache.redis")
public class RedisConfig {
private Duration timeToLive = Duration.ZERO;
/**
* 获取配置文件中配置的过期时间
* @param timeToLive
*/
public void setTimeToLive(Duration timeToLive) {
this.timeToLive = timeToLive;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
//Json序列化配置
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om=new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 高版本Springboot已经过期,用下面一行代码代替
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(om);
//String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive) //过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) //key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) //value的序列化方式
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
然后启动项目


控制台输出说明查询方法执行了
Redis中保存了查询的数据

刷新页面,控制台没有输出,说明是从缓存中查询到了数据
在配置文件中添加redis中数据的过期时间
spring:
cache:
redis:
time-to-live: 20000 #单位毫秒
把redis中的数据删除,重新启动项目
访问localhost:8080/emp/2
查看到redis中数据的TTL为13,说明还有13秒过期

数据过期之后再次访问localhost:8080/emp/2
看到重新执行了查询操作

本文介绍了如何在SpringBoot项目中使用缓存,并通过JSR-107规范理解缓存基本概念。详细讲解了Spring的@Cacheable、@CachePut和@CacheEvict注解的使用,以及如何配置Redis作为缓存存储,包括自定义CacheManage以解决序列化问题,并设置数据过期时间。
21万+

被折叠的 条评论
为什么被折叠?



