SpringBoot缓存
SpringBoot默认使用的是ConcurrentMapCache作缓存(由ConcurrentMapCacheManager创建),ConcurrentMapCache是将数据存储在ConcurrentMap< Object, Object>中
一、使用IDEA创建一个SpringBoot项目
创建的Springboot版本是1.5.15.RELEASE
二、项目开发
使用Mybatis操作数据库,在pom.xml中引入mybatis、Mysql和Cache依赖
<!-- cache依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- mybatis依赖 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- mysql连接依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
在数据库中创建两张表,employee和department,数据库的名字是springboot_cache
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;
在项目中创建名字为bean的package,编写employee表和department表对应的实体类,最好重写一下toString()方法
在项目中创建名字为mapper的package,用于存放EmployeeMapper和DepartmentMapper
public interface EmployeeMapper { @Select("select * from employee") public List<Employee> getAll(); @Select("select * from employee where id=#{id}") public Employee getById(Integer id); @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into employee(lastName, email, gender, d_id) values(#{lastName}, #{email}, #{gender}, #{dId})") public Integer save(Employee employee); @Update("update employee set lastName=#{lastName}, email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}") public Integer update(Employee employee); @Delete("delete from employee where id=#{id}") public Integer delete(Integer id); }
在启动类上添加@MapperScan注解和@EnableCache注解
// @MapperScan的作用是将指定包中的mapper扫描进spring容器中,也可以在每个mapper上添加@Mapper注解,作用相同 @MapperScan("com.example.cache.mapper") // @EnableCaching的作用是开启缓存 @EnableCaching @SpringBootApplication public class SpringBoot09CacheApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot09CacheApplication.class, args); } }
编写EmployeeService
@Service public class EmployeeService { @Autowired private EmployeeMapper employeeMapper; private final static Logger logger = LoggerFactory.getLogger(EmployeeService.class); @Cacheable(cacheNames="emp") public List<Employee> getAll() { logger.info("【获取所有员工信息】"); return employeeMapper.getAll(); } @Cacheable(cacheNames="emp") public Employee getById(Integer id) { logger.info("【获取id={}的员工信息】"); return employeeMapper.getById(id); } @CachePut(cacheNames="emp") public Integer save(Employee employee) { logger.info("【新增员工,{}】", employee); return employeeMapper.save(employee); } @CachePut(cacheNames="emp") public Integer update(Employee employee) { logger.info("更新员工信息,{}", employee); return employeeMapper.update(employee); } @CacheEvict(cacheNames="emp") public Integer delete(Integer id) { logger.info("删除id={}的员工信息", id); return employeeMapper.delete(id); } }
编写EmployeeController
@RestController public class EmployeeController { @Autowired private EmployeeService employeeService; @GetMapping("/emps") public List<Employee> getAll() { return employeeService.getAll(); } @GetMapping("/emp/{id}") public Employee getById(@PathVariable("id") Integer id) { return employeeService.getById(id); } @PostMapping("/emp") public Integer save(Employee employee) { return employeeService.save(employee); } @PutMapping("/emp") public Integer update(Employee employee) { return employeeService.update(employee); } @DeleteMapping("/emp/{id}") public Integer delete(@PathVariable("id") Integer id) { return employeeService.delete(id); } }
测试:
当我们第一次发出http://localhost:8080/emps 请求时会执行EmployeeService中的getAll()方法,但是之后再请求就不会执行,而是直接从缓存中获取数据,而不是从数据库中获取
三、Springboot中cache的使用详解
几个重要概念&缓存注解
概念图:
在Springboot中要使用cache,首先要在启动类上添加@EnableCaching注解,启用缓存
springboot中与cache有关的注解有五个,分别是@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig,下面我们解释一下这五个注解的作用
@Cacheable注解
@Cacheable执行的时机是在目标方法执行之前
作用是将方法的返回值存储在缓存中,以后再要相同的数据就直接从缓存中获取,不用执行方法操作数据库;
进入Cacheable源码中查看,可以发现Cacheable中有几个核心的属性,分别是:
- cacheNames/value:指定缓存组件的名字(必须有),cacheNames与value的作用一样,二选一使用;
每个CacheManager可以管理多个Cache组件(缓存组件),真正对缓存进行CRUD操作的是Cache组件;每一个Cache组件有一个唯一的名字,可以通过cacheNames/value指定;每个Cache组件中存储多个键值对(key/value),我们可以通过key获取value,而value中存放的是我们要存储的数据
// cache组件的名字为emp @Cacheable(cacheNames = "emp") public Employee getById(Integer id) { // ... }
- key:缓存数据使用的key,可以通过该属性指定;我们可以通过key获取存储在Cache中的数据;key默认是方法的参数的值,value默认是方法的返回值
我们也可以使用SpEL表达式自己指定key的名字:
// 使用方法的参数列表中名字为id的参数的值作为要存储数据的key,value为方法的返回值 @Cacheable(cacheNames = "emp", key = "#id") public Employee getById(Integer id) {
方法名[id]格式的key,例如:getById[2]
@Cacheable(cacheNames = "emp", key = "#root.methodName+'['+#id+']'") public Employee getById(Integer id) {
可以使用的SpEL表达式:
名字 位置 描述 示例 methodName root object 当前被调用的方法名 root.methodName method root object 当前被调用的方法 root.method.name target root object 当前被调用的目标对象 root.target targetClass root object 当前被调用的目标对象类 root.targetClass args root object 当前被调用的方法的参数列表 root.args[0] caches root object 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache root.caches[0].name argument name evaluation context 方法参数的名字. 可以直接#参数名,也可以使用#p0或#a0 的形式,0代表参数的索引; id、#username 、#a0、#p0 result evaluation context 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式’cache evict’的表达式beforeInvocation=false) result - keyGenerator:key的生成策略,可以自己写一个生成器放入到spring容器中,然后使用keyGenerator指定使用我们自己的生成器,key和keyGenerator两个属性二选一使用
自定义KeyGenerator:
import org.springframework.cache.interceptor.KeyGenerator; @Configuration public class CacheConfiguration { /** * 自己定义Cache中存储的数据的key的生成策略 * @return */ @Bean("myKeyGenerator") public KeyGenerator myKeyGenerator() { KeyGenerator keyGenerator = new KeyGenerator() { @Override public Object generate(Object o, Method method, Object... objects) { // return method.getName() + "[" + Arrays.asList(objects).get(0) + "]"; return method.getName() + "[" + SimpleKeyGenerator.generateKey(objects) + "]"; } }; return keyGenerator; } }
使用自定义的KeyGenerator:
@Cacheable(cacheNames = "emp",keyGenerator = "myKeyGenerator") public Employee getById(Integer id) {
cacheManager/cacheResolver:缓存管理器/缓存解析器,可以指定从哪个缓存管理器/缓存解析器中获取缓存组件,cacheManager和cacheResolver两个属性二选一使用
condition:满足指定条件的情况下才缓存,可以使用SpEl表达式,但是不能使用#result(使用condition=”#result!=null”不会生效)
// 参数id>0的情况下才缓存返回的数据 @Cacheable(cacheNames = "emp", key = "#id", condition = "#id>0") public Employee getById(Integer id) {
condition中也可以使用and、or、not、eq等逻辑表达式,例如:
condition = “#id>0 and #id<100 and #root.methodName eq ‘getById’”
- unless:否定缓存,与condition相反,当unless指定的条件为true时,方法的返回值不会被缓存,可以使用SpEl表达式,并且可以使用#result获取返回的结果
// 返回结果为null的情况下不缓存返回的数据 @Cacheable(cacheNames = "emp", key = "#id", unless = "#result==null") public Employee getById(Integer id) {
- sync:是否支持异步,异步模式下不支持unless
@CachePut注解
@CachePut执行的时机是在目标方法执行之后
作用是将既调用方法,又更新缓存数据,实现缓存和数据库的同步更新;在目标方法执行结束之后,将返回值存储在cache中
进入CachePut源码中查看,可以发现CachePut的属性与Cacheable中的属性是一样的,作用也都相同;但是CachePut与Cacheable中key属性是有区别的,CachePut中的key属性中可以使用#result,而Cacheable中的key属性中不能使用#result(#result不生效),原因是CachePut的执行时机是在目标方法执行完之后,所以可以得到result,而Cacheable的执行时机是在目标方法执行之前,此时是获取不到result的
还需要注意的是@CachePut中的key要与@Cacheable中的key一致,才能实现数据库与缓存的同步更新
错误的使用方式:
@Cacheable(cacheNames = "emp") public Employee getById(Integer id) { logger.info("【获取id={}的员工信息】"); return employeeMapper.getById(id); } @CachePut(cacheManager = "emp") public Employee update(Employee employee) { logger.info("【新增员工,{}】", employee); employeeMapper.save(employee); return employee; }
错误原因分析:
1、查询1号员工;查到的结果会放在缓存中;
key:1 value:employee对象(lastName=”zhangsan”; ….)
2、以后查询还是之前的结果,value:employee对象(lastName=”zhangsan”,….)
3、更新1号员工;【lastName:张三;gender:0】
将方法的返回值也放进缓存了;
key:传入的employee对象 value:employee对象(lastName:张三;gender:0; ….)
4、查询1号员工? 返回的是没有更新前的1号员工信息
为什么是没更新前的?【1号员工没有在缓存中更新】
原因是查询时的key与更新时使用的key不一样
解决办法:将CachePut中的key设置成 key = “#employee.id”或key = “#result.id” (@Cacheable的key是不能用#result)
正确的使用方式:
// 默认使用参数值作为key @Cacheable(cacheNames = "emp") public Employee getById(Integer id) { logger.info("【获取id={}的员工信息】"); return employeeMapper.getById(id); } // 同样使用id作为key,与Cacheable中的key保持一致 @CachePut(cacheManager = "emp", key="#employee.id") public Employee update(Employee employee) { logger.info("【新增员工,{}】", employee); employeeMapper.save(employee); return employee; }
@CacheEvict注解
@CacheEvict可以在目标方法之前或之后执行,可以通过属性beforeInvocation指定,默认是在目标方法执行完之后执行
作用是清除缓存
与Cacheable和CachePut相比,CacheEvict中多了两个属性,分别是allEntries和beforeInvocation
- allEntries:是否清除这个缓存中所有的数据,默认为false,为true时清除这个缓存中的所有数据;与key属性是二选一,key是清除指定的数据
- beforeInvocation:缓存的清除是否在目标方法之前执行,默认为false,表示在目标方法执行完之后执行清除缓存的操作,此时,如果目标方法执行的过程中出现异常,缓存就不会清除;为true时,表示在目标方法执行之前执行清除缓存操作,无论方法是否出现异常,缓存都会被清除
同样,CacheEvict中的key要与Cacheable中的key一致
@Cacheable(cacheNames = "emp", key="'emp_'+#id") public Employee getById(Integer id) { logger.info("【获取id={}的员工信息】"); return employeeMapper.getById(id); } @CacheEvict(cacheNames = "emp", key = "'emp_'+#id", allEntries = false, beforeInvocation = false) public Integer delete(Integer id) { logger.info("删除id={}的员工信息", id); return employeeMapper.delete(id); }
@Caching注解
@Caching是Cacheable、CachePut和CacheEvict三个注解的组合注解,用于定义复杂的缓存规则
java
@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注解
标注在类上面,作用是抽取这个类中的缓存的公共配置
// 表示该类中的所有缓存数据都从emp缓存组件中获取,类方法上的Cacheable、CachePut和CacheEvict注解就不需要再指定cacheNames了;CacheConfig中只有cacheNames属性而没有value属性(cacheNames与value属性的作用是一样的) @CacheConfig(cacheNames="emp") @Service public class EmployeeService { @Cacheable public List<Employee> getAll() { logger.info("【获取所有员工信息】"); return employeeMapper.getAll(); } }
四、原理分析
1、SpringBoot中的缓存自动配置类是CacheAutoConfiguration
2、缓存的配置类如下:
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
3、默认使用SimpleCacheConfiguration作为缓存的配置类
4、给容器中注册了一个CacheManager:ConcurrentMapCacheManager
5、可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中;
运行流程:
@Cacheable:
1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;
(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;
key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;
SimpleKeyGenerator生成key的默认策略:
如果没有参数;key=new SimpleKey();
如果有一个参数:key=参数的值
如果有多个参数:key=new SimpleKey(params);
3、没有查到缓存就执行目标方法,然后将目标方法返回的结果放进缓存中;如果查到了缓存就直接返回,不执行目标方法
@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,
如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据;
核心:
1)、使用CacheManager【默认是ConcurrentMapCacheManager】按照名字得到Cache【默认是ConcurrentMapCache】组件
2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator