文章目录
十一、SpringBoot与缓存
1.概念
(1).JSR107
Java Caching定义了五个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry
CachingProvider:
“缓存提供者”定义管理控制多个CacheManager,一个应用可以在运行期间访问多个CachingProvider
CacheManager:
“缓存管理器”定义控制管理多个Cache。一个CachingProvider对应一个CacheManager
Cache
将Entry的key-value对临时保存起来。一个CacheManager对应一个Cache
Entry
存储在Cache的key-value对
Expiry
“有效期”,对Cache保存时间的有效期可以在ExpiryPolicy中设置,超时就自动删除
(2).SpringBoot缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;
并支持使用JCache(JSR-107)注解简化我们开发;Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
2.Cache案例
(1)新建工程
(2)在数据库建表
DROP TABLE IF EXISTS department;
CREATE TABLE department (
id NUMBER(11),
departmentName varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS employee;
CREATE TABLE employee (
id NUMBER(11) ,
lastName varchar(255) DEFAULT NULL,
email varchar(255) DEFAULT NULL,
gender NUMBER(2) DEFAULT NULL,
d_id NUMBER(11) DEFAULT NULL,
PRIMARY KEY (id)
);
(3)创建一个package,然后创建属性对应的get set方法,进行数据封装
Department
package springboot.cache.bean;
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
package springboot.cache.bean;
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 + "]";
}
}
(4)整合Mybatis数据库
<1>写配置信息
spring:
datasource:
username: SYSTEM
password: 1
url: jdbc:oracle:thin:@127.0.0.1:1521:xe
driver-class-name: oracle.jdbc.OracleDriver
<2>使用注解版的Mybatis
@MapperScan自动进行扫描mapper接口所在的包(标记在运行类上)
@MapperScan("springboot.cache.mapper")
<3>编写接口,写出方法对应的SQL
@Mapper
public interface EmployeeMapper {
@Select("select * from Employee where id =#{id}")
public Employee getEmpById(Integer id);
@Insert("insert table employee (lastName,email,gender,d_Id) values(#{lastName},#{email},#{gender},#{dId})")
public void insertEmp(Employee employee);
@Update("update Employee set lastName=#{},email=#{},gender=#{},d_id=#{} where id=#{id}")
public void updateEmp(Employee employee);
@Delete("Delete from Employee where id=#{id}")
public void deleteEmp(Integer id);
}
<4>编写测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class CacheApplicationTests {
@Autowired
EmployeeMapper employeeMapper;
@Test
public void contextLoads() {
Employee employee = employeeMapper.getEmpById(1);
System.out.println(employee);
}
}
测试成功,接着写Service层和Controller层
Service
package springboot.cache.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import springboot.cache.bean.Employee;
import springboot.cache.mapper.EmployeeMapper;
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmp(Integer id) {
System.out.println("Number:"+id+"is selecting");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
}
Controller
package springboot.cache.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import springboot.cache.bean.Employee;
import springboot.cache.service.EmployeeService;
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id) {
Employee employee = employeeService.getEmp(id);
return employee;
}
}
然后测试,来到localhost:/8083/emp/id端口
测试成功。
(5)缓存体验
<1>开启基于注解的缓存
首先在主类上开启缓存功能
<2>标注缓存注解(Cacheable)
@Cacheable:根据方法的请求参数进行保存
@CacheEvict:清空缓存
@CachePut:调用+缓存方法
首先,在application.properties开启debug的时候会打印日志的功能
logging.level.springboot.cache.mapper=debug
#将mapper包下的debug行为进行log打印在控制台上
然后在需要缓存的地方标记上@Cacheable,这样就可以将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法
@Cacheable
public Employee getEmp(Integer id) {
System.out.println("Number:"+id+"is selecting");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
附上一张Cacheable内部构造,通常以Map形式进行存储
在EmployeeService上整理了一下笔记,然后进行了一下运行,检验一下我们的@Cacheable标签带来的作用。
这是通过数据库进行查询的结果
然后删除控制台日志,重新F5网页
发现没有打印新的日志,所以也就是说他内部也就没有再继续去查询数据库了,说明我们的缓存标签生效了。
<3>总结
Service层
/**
* 将方法的运行结果存储到缓存,以后再要相同的数据,就可以直接从缓存中提取,不用调用方法了
*
* CacheManager管理多个Cache组件,对缓存的真正的Crud操作在Cache组件中,每一个缓存组件都有一个自己的名字
*
* 属性解释:
* cacheName/value : 指定缓存组件的名字;讲方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存
*
* key:缓存数据使用的key,可以用它来指定,默认是使用方法参数的值 比如我们传入一个Integer id,那么这个id就是返回值
* 将id通过KV表达式保存起来
* #id表示参数的值,等同于#a0 #p0 #root.args[0]
*
* 以此为例@Cacheable(value="emp",key="#id")意思就是缓存组件名字是emp,缓存数据的key是id的值,#代表取出值
* 编写spel表达式的时候要知道 #id等同于#a0 #p0 #root.args[0]
* keyGenerator:key的生成器,自己制定key的生成id
* key/keyGenerator二选一
* 【此功能以cache.MycacheConfig为例】
* 编写自定义的keyGenerator,让Cacheable()内的keyGenerator可以取代key(二选一)
*
*
* cacheManager:指定缓存管理器或者使用cacheResolver指定获取解析器
* condition:指定符合条件的情况下才进行缓存
* condition = "#{id>0}"
* unless:否定缓存,当unless指定的条件为true的时候,方法的返回值就不会被缓存,可以获取到结果进行判断
* unless="#result==null"
* sync:是否使用异步模式
*
*
* 原理:
* 1.自动配置类CacheAutoConfiguration
* 2.配置缓存类
*
*
* 运行流程
* @Cacheable:
* 1.方法运行之前先去检查Cache(缓存组件),按照cacheNames指定的名字获取;
* (CacheManager先获取对应的缓存),第一次获取缓存如果没有Cache组件会自动创建
* 2.去Cache中查找缓存的内容,使用一个Key,默认就是方法的参数;
* key是按照某种策略生成的,默认就是使用KeyGenerator生成的
* SimpleGenerator生成key的策略:
* (1)如果没有参数:key=new Simple()
* (2)如果有一个参数:key=参数的值
* (3)如果有一个参数:key=new SimpleKey(params);
* 3.没有查到缓存就调用目标方法
* 4.将目标的方法返回的结果,放进缓存中
*
* @Cacheable标注的方法执行之前先检查缓存中是否存在,默认按照参数的值生成key去查询缓存,如果没有就将结果放入缓存。
*
*
* 核心:
* 1.使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCurrent】组件
* 2.key使用keyGenerator生成的,默认是SimpleKeyGenerator
* @param id
* @return
*/
Service层
@Cacheable(cacheNames = {"emp"},keyGenerator = "myKeyGeneratorConfig")
public Employee getEmp(Integer id) {
System.out.println("Number:"+id+"is selecting");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
Controller层
@GetMapping("/emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id) {
Employee employee = employeeService.getEmp(id);
return employee;
}
<4>CachePut
同步更新缓存
Service层
/**
* @CachePut:调用方法+更新缓存数据 修改数据库某个数据并且更新缓存
*
* 调用时机
* 1.先调用目标方法
* 2.将目标方法的结果缓存起来
*
* 测试步骤
* 1.查询1001号员工,结果会放到缓存中
* @存储的方法类似于KV存储方法,但是key是内部自然生成且自增的
* 2.以后查询还是之前的结果
* 3.即使缓存保留后,最数据库进行操作,也不影响缓存,缓存会自动进行更新操作
* @更新之后传入的实际上是一个employee的对象,value是更新的内容,而不是直接对应着,之前自动生成的key去更新
* @所以我们就要自动指定出来key,去做到真正的自动更新
*
* 例:key = "#employee.id":使用传入的参数的id
* key = "#result.id":使用返回的参数id
* 但是@cacheable的key不能用result,因为@cacheable是在查找前搜索id,而@CachePut可以用,因为是缓存完了更新完了去搜索id
*/
@CachePut(value = "emp",key = "#result.id")
public Employee updateEmp(Employee employee) {
employeeMapper.updateEmp(employee);
return employee;
}
Controller层
@GetMapping("/emp")
public Employee updateEmp(Employee employee) {
return employeeService.updateEmp(employee);
}
<5>CacheEvict
Service层
/**
* @CacheEvict : 缓存清除
* key:指定要清除的数据
* allEntries :选择是否删除所有缓存 true就是删除所有的了
* beforeInvocation = false : 缓存的清除是否在方法之前执行
* 默认代表是在方法执行之后执行,但是如果是true就是在执行方法之前清除缓存,不论方法是否有异常
*/
@CacheEvict(value = "emp")
public void deleteEmp(Integer id){
employeeMapper.deleteEmp(id);
}
Controller层
@GetMapping("/delemp")
public String deleteEmp(Integer id) {
employeeService.deleteEmp(id);
return "success";
}
成功删除缓存
<6>Caching
在这里我们以select 语句为例子,Caching更像是一个集合标签,可以在里面写上上面提到的三种方法
@Select("select * from Employee where lastName=#{lastName}")
public Employee getEmpbyLastName(String lastName);
Controller
@GetMapping("/emp/lastName/{lastName}")
public Employee getEmpByLastName(@PathVariable("lastName") String lastName) {
return employeeService.getEmpBylastName(lastName);
}
Service
@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);
}
<7>总结
我们发现上面所有都是从emp的缓存中提取数据,也就是value="emp"这样很麻烦,所以有一个其他的办法,让所有的标签固定的从一个地方来获取到同一张表的缓存,直接在类上加上@CacheConfig,进行集中配置,抽取缓存的公共属性
@CacheConfig(cacheNames = "emp")//抽取缓存的公共属性