Spring Boot与缓存
创建项目结构
- 集成开发工具 IDEA 2020.2 , 使用spring项目搭建向导创建
一、搭建基本环境
- 导入数据库文件,创建出department 和 employee表
create database springboot_cache;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
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;
- 创建javaBean封装数据
【Department.java】
public class Department {
private Integer id;
private String departmentName;
public Department() {
super();
// TODO Auto-generated constructor stub
}
public Department(Integer id, String departmentName) {
super();
this.id = id;
this.departmentName = departmentName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
@Override
public String toString() {
return "Department [id=" + id + ", departmentName=" + departmentName + "]";
}
}
【Employee.java】
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; //性别 1男 0女
private Integer dId;
public Employee() {
super();
}
public Employee(Integer id, String lastName, String email, Integer gender, Integer dId) {
super();
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.dId = dId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Integer getdId() {
return dId;
}
public void setdId(Integer dId) {
this.dId = dId;
}
@Override
public String toString() {
return "Employee [id=" + id + ", lastName=" + lastName + ", email=" + email + ", gender=" + gender + ", dId="
+ dId + "]";
}
}
-
整合MyBatis操作数据库
1)、配置数据源信息
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_cache?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#配置mybatis驼峰命名法规则
mybatis.configuration.map-underscore-to-camel-case=true
#开启缓存
logging.level.com.oy.springboot.mapper=debug
2)、使用注解版的Mybatis:
- @MapperScan 指定需要扫描的mapper接口所在的包
@MapperScan("com.oy.springboot.mapper")
@SpringBootApplication
public class SpringBoot01CacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot01CacheApplication.class, args);
}
}
二、快速体验缓存
步骤:
① 开启基于注解的缓存 @EnableCaching
@MapperScan("com.oy.springboot.mapper")
@SpringBootApplication
@EnableCaching // 开启注解
public class SpringBoot01CacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot01CacheApplication.class, args);
}
}
② 标注缓存注解即可
- @Cacheable
- @CacheEvict
- @CachePut
【EmployeeMapper】
@Mapper
public interface EmployeeMapper {
@Select("select * from employee where id =#{id}")
public Employee getEmpById(Integer id);
@Update("update employee set lastName=#{lastName},email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}")
public void updateEmp(Employee employee);
@Delete("delete from employee where id=#{id}")
public void insertEmpById(Integer id);
@Insert("insert into employee(lastName, email, gender, d_id) values(#{lastName}, #{email}, #{gender},#{dId})")
public void insertEmployee(Employee employee);
}
【EmployeeService】
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
@Cacheable(value = {"emp"})
public Employee getEmp(Integer id){
System.out.println("查询"+id + "号员工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
【EmployeeController】
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id){
Employee emp = employeeService.getEmp(id);
return emp;
}
}
测试:
- 第一次发送请求:
- 控制台输出
- 发送第二次请求,查看控制台没有发生改变(说明缓存生效)
三、缓存原理
① 重要的概念&缓存注解
注解 | 描述 |
---|---|
Cache | 缓存接口,定义缓存的操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache )组件 |
@Cacheable | 主要针对方法配置,能根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结构别缓存 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
② CacheManager
将方法的运行结果进行缓存:以后再要相同的数据,直接从缓存中获取,不在调用方法:
CacheManager管理多个Cache组件的,对缓存的真正CRUD(增删查改)操作在Cache组件中,每一个缓存组件有自己的唯一一个名字。
原理:
- 自动配置类:CacheAutoConfiguration
- 缓存的配置类:
org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默认】
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
- SimpleCacheConfiguration 配置类默认生效
在【application.properties】配置
debug=true
控制台输出日志:
- 给容器中注册一个CacheManager: ConcurrentMaCacheManager: 可以获取和创建ConcurrentMapCache类型的缓存组件;它的作用将数据保存砸ConcurrentMap中。
③ @Cacheble
运行流程:
-
方法运行之前,先去查看Cache(缓存组件),按照cacheName指定的名字获取;(CacheManager先获取相对应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
-
去Cache中查找缓存的内容,使用一个key,默认一个key,默认就是方法的参数;
key是按照某种策略生成的,默认是使用keyGenerator生成的key:
SimpleKeyGenerator生成key的默认策略:
如果没有参数; key=new SimpleKey()
如果一个参数: key=参数的值
如果有多个参数: key=new SimpleKey(params);
-
没有查到缓存就调用目标方法:
-
将目标方法返回的结果,放进缓存中
@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据。
核心:
- 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
- key使用keyGenerator生成的,默认是SimpleKeyGenerator
属性:
- CacheNames/value: 指定缓存组件的名字;将方法的返回结果放在哪个缓冲区,是数组的方式,可以指定多个缓存:
- key: 缓存数据使用key: 可以用它来指定,默认是使用方法参数 1-方法的返回值
- 编写SqEL: #id; 参数id的值 #a0 #p0 #root.args[0] getEmp[2]
- keyGenerator: key的生成器:可以自己指定key的生成器的组件id
- key / keyGenerator: 二选一使用
- cacheManager: 指定的缓存管理器: 或者cacheResolver指定获取解析器
- condition: 指定符号条件的情况下才缓存:
- eg: condition = “#a0 >1” : 第一个参数的值 > 1的时候才会被缓存; 可以获取到结果进行判断
- unless: 否定缓存: 当unless指定的条件为true, 方法的返回值就不会被缓存;可以获取到结果进行判断
- unless = “#result == null”
- unless = “#a0 ==2”: 如果第一个参数的值为2,结果不缓存;
- sync: 是否使用异步模式
示例:
@Cacheable(value = {"emp"},keyGenerator = "myKeyGenerator",condition = "#a0>1",unless = "#a0==2")
public Employee getEmp(Integer id){
System.out.println("查询"+id+"号员工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
注意使用keyGenerator需要自行配置(参考):
【keyGenerator】以getEmp[2] 为例
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... objects) {
// 拼接getEmp[2] 作为keyGenerator
return method.getName() + "["+ Arrays.asList(objects).toString()+"]";
}
};
}
}
④ @CachePut
即调用目标的方法,有更新缓存数据;同步更新缓存
修改了数据库的某个数据,同时更新缓存;
运行机制:
- 先调用目标方法
- 将目标方法的结果缓存起来
测试步骤:
- 查询1号员工: 查到的结果会放在缓存中:
- 更新1号员工:【lastName: AAA; gender:0】
- 在次查询1号员工(缓存没有更新)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UkuC1EgW-1601525171281)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20200917170832851.png)]
解决方式:
@CachePut(value = "emp", key = "#result.id")
public Employee updateEmp(Employee employee){
System.out.println("updateEmp:"+ employee);
employeeMapper.updateEmp(employee);
return employee;
}
可设置参数约束条件:
-
key : 传入的employee对象 值: 返回的employee对象
- key = “#employee.id”: 使用返回后的id
- key ="#result.id": 使用返回后的id
注意:@Cacheable的key是不能用#result
⑤ @CacheEvict
缓存清除
key: 指定要清除的数据
beforeInvocaion = false: 缓存的清除是否在方法之前执行
默认代表缓存清除的操作是在方法执行之后;如果出现异常缓存就不会清除
beforeInvocation = true: 代表清除缓存操作在方法运行之前执行,无论方法是否出现异常,缓存都要清除。
@CacheEvict(value = "emp", key = "#id")
public void deleteEmp(Integer id) {
System.out.println("deleteEmp:" + id);
//employeeMapper.deleteEmp(id);
//模拟异常
//int i = 10/0;
}
⑥ @Caching
定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(/*value="emp",*/key = "#lastName")
},
put = {
@CachePut(/*value="emp",*/key = "#result.id"),
@CachePut(/*value="emp",*/key = "#result.email")
}
)
public Employee getEmpByLastName(String lastName){
return employeeMapper.getEmpByLastName(lastName);
}
⑦ CacheConfig
抽取缓存的公共配置
@CacheConfig(cacheNames="emp"/*,cacheManager = "employeeCacheManager"*/)
@Service
public class EmployeeService {
四、整合Redis 作为缓存
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以作数据库、缓存和消息中间件。
- 安装redis: 使用Docker
docker pull redis
- 引入redis的starter
docker run -d -p 6379:6379 --name myredis redis
- 配置redis (下载软件RedisDesktopManager)
4、Redis常见 的五大数据类型
- String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)
- stringRedisTemplate.opsForList()[List(列表)]
- stringRedisTemplate.opsForSet()[Set(集合)]
- stringRedisTemplate.opsForHash()[Hash(散列)]
- stringRedisTemplate.opsForZSet()[ZSet(有序集合)]
5、测试缓存
原理:CacheManager === Cache 缓存组件来实际缓存中存取数据
① 引入redis的starter,容器中保存的是 RedisCacheManager;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在配置文件中配置
spring.redis.host=192.168.64.129
② RedisCacheManager 帮我们创建 RedisCache 来作为缓存组件;RedisCache 通过操作redis缓存数据的
@Autowired
StringRedisTemplate stringRedisTemplate; // 操作k-v都是字符串的
@Autowired
RedisTemplate redisTemplate;// k-v都是对象的
@Test
public void test1(){
// 给redis中保存数据
stringRedisTemplate.opsForValue().append("msg","hello");
String msg = stringRedisTemplate.opsForValue().get("msg");
System.out.println(msg);
stringRedisTemplate.opsForList().leftPush("mylist","1");
stringRedisTemplate.opsForList().leftPush("mylist","2");
}
③ 默认保存数据 k-v 都是Object; 利用序列化保存;
1)引入了redis的starter, cacheManager变为 RedisChacheManager;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2) 默认创建的 RedisCacheManager 操作redis的时候使用的是 RedisTemplate<Object, Object>
@Autowired
EmployeeMapper employeeMapper;
@Autowired
StringRedisTemplate stringRedisTemplate; // 操作k-v都是字符串的
@Autowired
RedisTemplate redisTemplate;// k-v都是对象的
// 测试保存对象
@Test
public void test02(){
Employee empById = employeeMapper.getEmpById(1);
redisTemplate.opsForValue().set("emp-01",empById);
}
3) RedisTemplate<Object, Object> 是默认使用jdk 的序列化机制
【MyRedisTemplateConfig】
@Configuration
public class MyRedisTemplateConfig {
@Bean
public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object, Employee> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<>(Employee.class);
template.setDefaultSerializer(ser);
return template;
}
}
测试:
@Resource
RedisTemplate<Object, Employee> employeeRedisTemplate;
@Test
public void test02(){
Employee empById = employeeMapper.getEmpById(1);
// 1.将数据以 JSON的方式保存
// 2. redisTemplate默认的序列化规则,改变默认的序列化规则
employeeRedisTemplate.opsForValue().set("emp-01",empById);
}
①自定义CacheManager
1、 SpringBoot 1.x版本的RedisCacheManager配置
//自定义CacheManager
@Bean
public RedisCacheManager empCacheManager(RedisTemplate<Object, Employee> empRedisTemplate) {
//将我们自定义的RedisTemplate作为参数,Spring会自动为我们注入
RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate);
//使用前缀,默认会将CacheName作为key的前缀,最好设置为true,因为缓存可能有很多类
cacheManager.setUsePrefix(true);
return cacheManager;
}
}
但是如果我们仅仅自定义这一个CacheManager则只能操作Employee这一种类型的数据,因为这个CacheMananger只实现了Employee的泛型,操作其他类型就会报错(可以正常缓存其他类型的数据,但是从缓存中查询出的数据在反序列化时会报错)。这时我们就需要自定义多个CacheManager,比如增加一个可以缓存Department类型的CacheMananger:
@Bean
public RedisCacheManager deptCacheManager(RedisTemplate<Object, Department> deptRedisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(deptRedisTemplate);
//使用前缀,默认会将CacheName作为key的前缀
cacheManager.setUsePrefix(true);
return cacheManager;
}
当容器中有多个RedisCacheManager的时候,需要使用@Primary指定一个默认的
2、SpringBoot 2.x版本的RedisCacheManager配置
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存过期时间为一天
.entryTtl(Duration.ofDays(1))
.disableCachingNullValues() // 禁用缓存空值,不缓存null校验
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new
GenericJackson2JsonRedisSerializer()));// 设置CacheManager的值序列化方式为json序列化,可加入@Class属性
// 设置默认的cache组件
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfiguration).build();
}
3、测试
@Cacheable(cacheNames = "dept",cacheManager ="cacheManager")
public Department getDeptById(Integer id) {
System.out.println("查询部门:" + id);
Department department = deptMapper.getDeptById(id);
return department;
}