JSR107规范
玩Java要有仪式感,因此先来看看JSR107对缓存的规范。在Java Caching定义了5个核心接口
- Entry:存储在Cache的键值对
- Cache:K ——V结构,以key为索引值,一个Cache只能被一个CacheManager所拥有
- CacheManager:顾名思义就是对Cache的管理,比如创建,配置,获取,管理和控制这些K——V结构的Cache。
- CachingProvider:用来管理CacheManager
- Expiry:定义有效期相关
他们的关系就如下图所示
用户可以一次访问一个或多个CacheProvider,一个Provider下又有一个或者多个CacheManager,CahceManager又对Cache进行管理。其实吧,我认为他这个分层有些冗余。
正因为这种缓存规范的不合理,因此很多情况下都会用Spring的缓存抽象
Spring的缓存抽象
在Spring中主要用了JSR107的Cache,CacheManager来管理缓存,当然如果要使用完整的JSR107规范也可以。
下面是Spring中提供的一些缓存注解
@Cacheable和@CachePut的区别就是,Cacheable对请求方法进行缓存,而CachePut既保证方法被调用又要缓存,因此前者一般用来加缓存,后者一般来更新缓存。
在使用这些缓存注解前,要先开启基于注解的缓存,并且设定key的生成策略和对value的序列化(到底是使用简单字符串还是JSON等等)。
缓存初体验
- 建立实体类
Employee
//实现序列化接口是为了后边reids可以缓存对象
public class Employee implements Serializable {
private Integer id;
private String lastName;
private String email;
private Integer gender; //性别 1男 0女
private Integer dId;
//getter
//setter
}
Department
public class Department {
private Integer id;
private String departmentName;
//getter
//setter
- 配置Mysql数据库连接
spring:
datasource:
username: root
url: jdbc:mysql:///cache?serverTimezone=GMT&useSSL=FALSE
password: wrial.qq.com
type: com.zaxxer.hikari.HikariDataSource
#开启驼峰命名
mybatis:
configuration:
map-underscore-to-camel-case: true
#自动配置报告
#debug=true
#Redis,配置的是虚拟机上的redis
spring.redis.host=1270.0.01
- 编写Mapper
package com.wrial.cahcedemo.mapper;
import com.wrial.cahcedemo.dao.Employee;
import org.apache.ibatis.annotations.*;
@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 deleteEmp(Integer id);
@Insert("insert into employee(lastName,email,gender,d_id) values(#{lastName},#{email},#{gender},#{dId})")
public void insertEmp(Employee employee);
@Select("select * from employee where lastName = #{lastName}")
Employee getEmpByLastName(String lastName);
}
- 开启缓存注解
@MapperScan("com.wrial.cahcedemo.mapper")
@SpringBootApplication
@EnableCaching//开启缓存注解
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class, args);
}
}
- 编写Service
@Cacheable:可缓存
* 缓存说明* 默认的采用SimpleCache,底层使用ConconcurrentMap。
* cacheNames/value:二选一,指定缓存名,可以是一个数组类型
* CacheManager:很多个缓存名所标识的缓存都让CacheManager来管理,也可以指定缓存管理器(如Redis,Mem,或者ConcurrentHashMap等)
* CacheResolver:和缓存管理器二选一
* key:缓存使用的key,默认是传入的参数,也可以自定义keyGenerator,也可以使用Spel表达式
* keyGenerator():key的生成器,默认以传入的参数为key,也可以自定义,和key二选一(有了key就不需要KeyGenerator来自动生成key了)
* Condition:可以指定条件,也可以使用Spel(如#{id>0})
* Unless:否定缓存,当Unless为true,那就不会缓存,
* 也可以获取到结果再判断(如#{result==null}),作用和Condition相反,但是可以一起使用进行双重判断
* sysc:是否异步,用了以不就不能用unless了
@CachePut:既调用方法,又更新数据
* 修改了数据库某个值同步更新
*能导致缓存同步失败的几个原因?
* 1.value不相同
* 2.key不相同,在查询的时候是以id为key,如果update以employee对象key就会导致同步失败
* 也可以使用#result.id
* 和Cacheable的区别:Cacheable主要是在缓存前起作用进行一些列判断要不要缓存
* 而cacheput是缓存后起作用,因此Cacheable不能使用result。
* 测试步骤:
* 1.先查询
* 2.update
* 3.查询
@CacheEvict 删除缓存
* key 指定要删除的元素
* allEntries = false 默认为false,如果为true删除当前value/name下的所有缓存
* beforeInvocation = false 默认在方法执行之后删除数据,
* 如果为true,那就在执行方法之前(可以在方法中写一个异常,查看是否清除异常
@Caching Cacheable,CachePut和CahceEvict的集合
* public @interface Caching {
* Cacheable[] cacheable() default {};
* CachePut[] put() default {};
* CacheEvict[] evict() default {};
@Service
//@CacheConfig//顾名思义,写在类上就是可以批量的声明某一类缓存,
// 或者是按照某一种生成更改规则,不用在每一个方法上写,减少了代码的重复性
@Slf4j
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
//使用默认的缓存Key生成策略,也就是传入的参数
@Cacheable(value = "#{emp}", key = "#id")//标识可缓存(运行结果)
public Employee getEmp(Integer id) {
return employeeMapper.getEmpById(id);
}
//使用Spel自定义Key,比如自定义生成方法名和id的组合
@Cacheable(cacheNames = "#{emp}", key = "#root.methodName+'['+'#{id}'+']'")
public Employee getEmp02(Integer id) {
return employeeMapper.getEmpById(id);
}
//使用KeyGenerator自定义Key,比如自定义生成方法名和id的组合myKeyGenerator是自定义到bean里的
@Cacheable(cacheNames = "#{emp}", keyGenerator = "myKeyGenerator")
public Employee getEmp03(Integer id) {
return employeeMapper.getEmpById(id);
}
//Condition,如果第一个参数大于1才给缓存
@Cacheable(cacheNames = "#{emp}", condition = "#a0>1")
public Employee getEmp04(Integer id) {
return employeeMapper.getEmpById(id);
}
//如果第一个参数大于1才给缓存
@Cacheable(cacheNames = "#{emp}", condition = "#a0>1", unless = "#a0==2")
public Employee getEmp05(Integer id) {
return employeeMapper.getEmpById(id);
}
@CachePut(value = "#{emp}", key = "#employee.id")
public Employee updateEmp(Employee employee) {
log.info("员工更新信息-》{}", employee);
employeeMapper.updateEmp(employee);
return employee;
}
@CacheEvict(value = "#{emp}", key = "#id")
public void deleteEmp(Integer id) {
System.out.println("deleteEmp" + id);
// employeeMapper.deleteEmp(id);
}
@CacheEvict(value = "#{emp}", allEntries = true)
public void deleteAllEmp() {
System.out.println("deleteAllEmp");
}
@CacheEvict(value = "#{emp}", beforeInvocation = true, key = "#id")
public void deleteEmpBefore(Integer id) {
int i = 1 / 0;
System.out.println("deleteEmpBefore");
}
* 可以满足复杂的缓存条件
* */
@Caching(cacheable = {
@Cacheable(value = "emp",key = "#lastName")
},
put = {
@CachePut(value = "emp",key = "#result.lastName"),
@CachePut(value = "#{emp}",key = "#result.id")//设置为#{emp}是为了测试上边的查询方法
})
public Employee getEmpByLastName(String lastName) {
return employeeMapper.getEmpByLastName(lastName);
}
}
Spel表达式获取指定数据的方法
- 编写controller
@Slf4j
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id) {
Employee emp = employeeService.getEmp(id);
return emp;
}
//测试自定义缓存策略
@GetMapping("/emp01/{id}")
public Employee getEmployee01(@PathVariable("id") Integer id) {
Employee emp = employeeService.getEmp02(id);
return emp;
}
//测试自定义KeyGenerator
@GetMapping("/emp02/{id}")
public Employee getEmployee02(@PathVariable("id") Integer id) {
Employee emp = employeeService.getEmp03(id);
return emp;
}
//测试Condition,如果id大于1才给缓存
@GetMapping("/emp03/{id}")
public Employee getEmployee03(@PathVariable("id") Integer id) {
Employee emp = employeeService.getEmp04(id);
return emp;
}
//测试Condition,如果id大于1才给缓存,unless如果满足就不缓存,因此一号二号都不缓存
@GetMapping("/emp04/{id}")
public Employee getEmployee04(@PathVariable("id") Integer id) {
Employee emp = employeeService.getEmp05(id);
return emp;
}
@GetMapping("/emp/update")
public Employee updateEmp(Employee employee) {
Employee employee1 = employeeService.updateEmp(employee);
return employee1;
}
//根据特定id删除指定缓存
@GetMapping("/emp/delete/{id}")
public void deleteEmp(@PathVariable("id") Integer id) {
employeeService.deleteEmp(id);
log.info("success!");
}
//删除所有缓存数据
@GetMapping("/emp/deleteAll")
public void deleteAllEmp() {
employeeService.deleteAllEmp();
log.info("deleteAllEmp Success!");
}
//在方法执行之前删除缓存
@GetMapping("/emp/deleteBefore/{id}")
public void deleteEmpBefore(@PathVariable("id") Integer id) {
employeeService.deleteEmpBefore(id);
log.info("deleteBefore!");
}
@GetMapping("/emp/lastName/{lastName}")
public void getEmpByLastName(@PathVariable("lastName") String lastName) {
Employee empByLastName = employeeService.getEmpByLastName(lastName);
log.info("empByLastName->{}", empByLastName);
}
}
下图就是Spring缓存策略结构图
测试:http://localhost:8080/emp/2
第一次访问
第二次访问就不会从数据库了
当然可以添加自己的设置。
SpringBoot Cache自动配置原理
SpringBoot是怎样对在properties中自动配置不同缓存策略进行自动配置的?
思路:对于SpringBoot的自动配置,一般的命名方式都是xxxAutoConfiguration
- 查看CahceAutoConfiguration,并且找到对这些缓存组件加载的方法(Selector)
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
- 进行断点调试,并查看类加载的信息
可以看到调试信息,下面都是一些缓存组件,比如JCACHE(JSR107),EHCACHE,REDIS等等
这些都是在mapping添加的
根据容器中存在Cahce和CacheCondition,在没有CacheManager的使用配置
@Configuration
@ConditionalOnBean(Cache.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class GenericCacheConfiguration {
private final CacheManagerCustomizers customizers;
GenericCacheConfiguration(CacheManagerCustomizers customizers) {
this.customizers = customizers;
}
@Bean
public SimpleCacheManager cacheManager(Collection<Cache> caches) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caches);
return this.customizers.customize(cacheManager);
}
@Cacheabble的运行流程
3. 第一次CacheManager先去Cache中去查找(缓存组件),按照cacheNames指定的缓存名获取,如果没有此缓存就创建一个Cache组件
4. 使用key去Cache中查找缓存内容(默认是方法使用SimpleKeyGenerator定义生成key策略)
5. 没有查询到缓存就调用目标方法
下一次在实战的时候给它会加上缓存的格式是JSON格式还是其他格式的配置