主要内容
1. JSR-107缓存规范
2. Spring缓存抽象
3. 整合Redis
1. 基本环境搭建
1. 创建项目
1. 模块选择
2. 导入数据库文件
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;
3. 编写实体类
4. 整合durid连接池
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/spring_cache?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always
schema:
- classpath*:department.sql
type : com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
test WhileIdle: true
test OnBorrow: false
test OnReturn: false
poolPreparedStatements: true
filters: stat ,wall,slf4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true ;druid.stat.slowSqlMillis=500
mybatis:
configuration:
map-underscore-to-camel-case: true
5. 整合mybati操作数据库 --- 使用注解版的MyBatis
1. @MapperScan指定需要扫描的mapper接口所在的包
@MapperScan("com.atguigu.springboot.cache.bean.mapper" )
@SpringBootApplication
public class SpringbootAdvanced01ChcheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAdvanced01ChcheApplication.class, args);
}
}
2. 两个mapper接口
@Mapper //声明这是mybatis的一个mapper类
public interface DepartmentMapper {
}
@Mapper //声明这是mybatis的一个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 deleteEmpById(Integer id);
@Insert("insert into employee(lastName, email, gender, d_id) values(#{lastName}, email=#{email}, #{gender}, #{dId})" )
public void insertEmployee(Employee employee);
}
3. EmployeeService
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
4. 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;
}
}
5. 测试一下
2. 缓存使用
步骤:
1. 开启基于注解的缓存
@EnableCaching :标注在主配置类上
2. 标注缓存注解即可
@Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict:清空缓存
@CachePut: 保证方法被调用,又希望结果被缓存。
2.1 没有开启缓存--测试
1. 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;
}
}
2. 开启mapper包的日志
将相关日志打印出来:包括sql语句
我写在了application.properties
logging.level.com.atguigu.springboot.cache.mapper=debug
3. 开启缓存之前,测试:发送若干次同一个请求
结果:
每次发送同一个请求,都会执行一次相同的方法,得到相同的结果。
没有对其结果进行缓存。
2.2 开启缓存@Cacheable
将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
不用在调用数据库,进行sql查询了。
1. 在EmployeeService的方法中标注:
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
/**
* 讲方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
*
* CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一的一个名字
* Cache中数据以key-value对的数据存储。
* 几个属性:
* cacheNames/value:指定缓存组件的名字
* key:缓存数据时,数据的key。不指定时默认是使用方法参数的值
* 比如参数传个1,结果返回user。
* 那么存到cache中的就是:1-user。
*
* 自己指定:编写SpEl表达式
* key="#id" :取出参数id的值,把这个值作为key
*
* @param id
* @return
*/
@Cacheable(cacheNames = "emp" , key="#id" )
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
2. @Cacheable(cacheNames = {"emp", "temp"}, key="#id", condition = "#a0>0", unless = "#result == null")
分析一下缓存注解
1. 作用
讲方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
2. cacheNames/value属性
CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,
每一个缓存组件有自己唯一的一个名字。
cacheNames/value:指定缓存组件的名字
3. key属性 key="#id"/key="#root.aggs[0]"
Cache中数据以key-value对的数据存储。
指定缓存数据时,数据的key,不指定时默认是使用方法参数的值。
比如参数传个1,结果返回user。那么存到cache中的就是:1-user。
通过编写SpEl表达式,来指定key。
key="#id":取出参数id的值,把这个值作为key
等价于:key="#root.aggs[0]", #a0, #p0
相关的SpEl写法
4. cacheManager:指定相关的缓存管理器
5. cacheResolver: 缓存解析器。类似缓存管理器,配置时两者二选一。
6. condition:判断条件,指定符合条件的情况下才缓存。
condition = "#a0>0",
7. keyGenerator:key的生成器;
指定了key,就不要指定keyGenerator了。二选一
8. unless:否定缓存,当unless指定的条件为true,方法的返回值就不会缓存。
可以对获取到的结果进行判断。
unless = "#result == null"
9. sync:是否使用异步模式
3. 测试一下
3. 原理
自动配置类 : CacheAutoConfiguration
3.1 第一次请求
1. 点进去
2. 导入一个CacheConfigurationImportSelector
@Import({ CacheConfigurationImportSelector.class,
3. 点过去,打上断点分析一下。
4. 返回了十个缓存配置类
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.SimpleCacheConfiguration
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
5. 这么多配置类,哪个配置类能生效呢?
# 打开自动配置报告
debug: true
在控制台搜索一下CacheConfiguration。
6. 发现只有一个SimpleCacheConfiguration默认生效了
SimpleCacheConfiguration matched
CaffeineCacheConfiguration:Did not match:
CouchbaseCacheConfiguration:Did not match:
EhCacheCacheConfiguration: Did not match:
GenericCacheConfiguration:Did not match:
HazelcastCacheConfiguration:Did not match:
InfinispanCacheConfiguration:Did not match:
JCacheCacheConfiguration:Did not match:
NoOpCacheConfiguration:Did not match:
RedisCacheConfiguration:Did not match:
7. 进入SimpleCacheConfiguration分析一下
给容器中注册了一个:ConcurrentMapCacheManager cacheManager
@Configuration(proxyBeanMethods = false )
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}
}
7.1 进入ConcurrentMapCacheManager看一下
实现了CacheManager
7.1.1 CacheManager中一个方法
Cache getCache(String var1);
按照一个名字得到一个Cache缓存组件
7.2 ConcurrentMapCacheManager重写了getCache()方法
按照一个名字得到一个Cache缓存组件
@Nullable
public Cache getCache(String name) {
Cache cache = (Cache)this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized(this.cacheMap) {
cache = (Cache)this.cacheMap.get(name);
if (cache == null) {
cache = this.createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
7.3 分析一下该方法
大概分析一下:按照一个名字得到一个Cache缓存组件
@Nullable
public Cache getCache(String name) {
Cache cache = (Cache)this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized(this.cacheMap) {
cache = (Cache)this.cacheMap.get(name);
if (cache == null) {
cache = this.createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
7.3.1
Cache cache = (Cache)this.cacheMap.get(name);
cacheMap是ConcurrentMap<String, Cache>类型的Map
所以就是从cacheMap中根据cacheName获取ConcurrentMapCache类型的Cache
7.3.2
if (cache == null && this.dynamic){
//如果没有将创建一个ConcurrentMapCache类型的Cache
cache = (Cache)this.cacheMap.get(name);
}
7.4 SimpleCacheConfiguration的作用
向容器中注入ConcurrentMapCacheManager缓存管理器
ConcurrentMapCacheManager缓存管理器可以获取和创建ConcurrentMapCache类型的Cache缓存组件。
8. 看一下ConcurrentMapCache类型的缓存组件
里面有一些操作缓存的方法
比如:获取缓存中的数据和将数据存入缓存中
private final ConcurrentMap<Object, Object> store;
protected Object lookup(Object key) {
return this.store.get(key);
}
public void put(Object key, @Nullable Object value) {
this.store.put(key, this.toStoreValue(value));
}
store就是一个ConcurrentMap
8.1 我们可以认为它将数据保存在ConcurrentMap,从这个Map中存取数据
4. 分析一下缓存的执行流程
以@Cacheable为例
在Service方法里面打上断点。
ConcurrentMapCache
源码中的存取数据打上断点。
ConcurrentMapCacheManager
1. debug模式启动,发送一个请求
进入断点
2. 发现发送请求后,没有进入service方法中,而是先来到缓存管理器的获取缓存的方法
ConcurrentMapCacheManager
(Cache)this.cacheMap.get(name);
先去找name是emp的缓存
emp就是我们在service方法中指定的cache的name。
3. 由于是第一次发送请求所以还没有name是emp的缓存
所以创建一个。
把创建出来的缓存组件都放到cacheMap中。
得到一个cache缓存组件。
4. 放行来到了ConcurrentMapCache 中的 lookup(Object key)
按照key去缓存中找值
protected Object lookup(Object key) {
return this.store.get(key);
}
按照key=1来获取cache中的数据。
1是我们请求中传过来的参数。
5. key=1是怎么得到的呢
keyGenerator.generate(this.target, this.metadata.method, this.args);
通过key生成器,将对象,方法,参数都传过来生成的。
所以默认是使用keyGenerator生成的key。
6. keyGenerator是一个接口,默认是SimplekeyGenerator实现
SimplekeyGenerator生成key策略:
如果没有参数:key=new SimpleKey()
如果有一个参数:key=参数值
如果有多个参数:key=new SimpleKey(params)
7. 回到4步的lookup(Object key)
查询,由于是第一次,缓存没数据。
查到的value是个空值。
8. 放行
来到了目标方法。
service中的方法。
9. 放行
将目标方法的返回值放进cache中。
store是ConcurrentMap数据类型的。所以默认用的是ConcurrentMap来缓存数据。
10. 所以@Cache标注的方法,执行之前先来检查缓存。
默认按照参数的值作为key来查询缓存。
如果没有就运行方法并将结果放入缓存。
11. 存好之后,显示页面
3.2 第二次请求
1. 现根据缓存name获取缓存
2. 根据生成的key去缓存中取数据
3. 取到数据,放行。直接页面显示
没有调用service中的方法
3.3 小结
1. 使用CacheManager 按照名字Cache的name属性得到Cache组件
默认是ConcurrentMapCacheManager实现CacheManager 接口
用ConcurrentHashMap容器来存取容器
2. key是使用keyGenerator生成的
3. 先检查缓存,缓存中没有数据再去执行目标方法。
3.3. @Cacheable 的属性用法
@Cacheable放在方法上
1. cacheName/value
将数据放到名为XX的缓存cache中,可以同时指定多个cache名。
@Cacheable(cacheNames = {"emp", "temp"})
2. key
cache中的数据以key-value的形式存储。
默认是方法的参数值,我们也可以通过SpEL表达式来手动指定
例子: key="#root.methodName+'['+#id+']'"
方法名拼接参数,key=getEmp[id]
注意: @Cacheable中的key不能用@result.id这种形式指定。
因为@Cacheable标注的方法在运行之前,就通过指定的key去缓存中查找。
所以方法还没运行之前,还没有result,就要得到key。
所以不能通过result得到key。
3. keyGenerator:key的生成器
key就是由keyGenerator生成的,生成规则:
如果没有参数:key=new SimpleKey()
如果有一个参数:key=参数值
如果有多个参数:key=new SimpleKey(params)
key和keyGenerator只需指定其中一个即可。
我们自定义一个keyGenerator,注意包不要导错。
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator" ) //加在容器中
public KeyGenerator keyGenerator (){
return new KeyGenerator (){
@Override
public Object generate(Object o, Method method, Object... params) {
return method.getName()+"[" + Arrays.asList(params).toString()+"]" ;
}
};
}
}
添加了keyGenerator,就不用再添加key属性了。
@Cacheable(cacheNames = {"emp", "temp"}, keyGenerator = "myKeyGenerator")
测试一下:这是我们自己指定的有参时的Key的生成策略。
4. CacheManager:后面整合多个缓存管理器时再说。
5. condition:符合条件时,才缓存方法返回的结果
condition = "#a0>0"
方法传过来的参数中第一个参数值大于0时,才缓存返回结果。
condition = "#a0>0 and #root.getMethodName() eq 'getEmp'"
第一个参数要大于0,并且目标方法名为getEmp
6. unless:满足条件是不缓存
unless = "#result == null" 结果为null时,不缓存
4. @CachePut:
目标方法调用之后,再放到缓存中。
可以通过指定和@Cacheable相同的key,实现即调用方法,又更新缓存数据。
修改了数据库的某个数据,同时更新缓存。
@Cacheable(cacheNames = {"emp" , "temp" }, keyGenerator = "myKeyGenerator" )
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
@CachePut(value = "emp" )
public Employee updateEmp(Employee employee){
System.out.println("updateEmp: " + employee);
employeeMapper.updateEmp(employee);
return employee;
}
1. 测试一下:
测试步骤:
1. 查询1号员工:查到的结果会放到缓存中
2. 以后查询1号员工,还是原来的结果
3. 更新一号员工,更新后的结果放到了缓存中
4. 再来查询1号员工:竟然是没有更新前的数据。
原因: 因为第一次查询1号员工将数据放到缓存中是以key-value的形式,我们指定了key:
getEmp[[1]] --- 结果。
更新1号员工后,将方法的返回值也放到缓存中,那么缓存中的数据的key,
默认是参数:
传入的employee --- 结果。
所以即使更新1号员工后在查询1号员工,得到的结果是更新前的数据。
2. 想要实现查询1号员工的结果是更新后的数据。
key="#result.id" 将更新后的结果存在cache中且key是employee.id。
将查询方法和更新方法指定同样的返回值和同样的key。
//查询
@Cacheable(cacheNames = {"emp" , "temp" })
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
//更新
@CachePut(value = "emp" , key="#result.id" )
public Employee updateEmp(Employee employee){
System.out.println("updateEmp: " + employee);
employeeMapper.updateEmp(employee);
return employee;
}
3. @CachePut通过指定和@Cacheable相同的key,达到了即调用方法,又更新缓存数据。
4. 注意一个Bug.
如果我发送的请求是:http://localhost:8080/emp?lastName=王五
Controller:
接收到参数封装成一个employee
Employee [id=null, lastName=王五, email=null, gender=null, dId=null]
employee传给Service
employeeMapper.updateEmp(employee)
employee传给Mapper
@Update("update employee set lastName=#{lastName}, email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}")
public void updateEmp(Employee employee);
最后在Mapper的逻辑:
找到id=#{id}的记录,将其更新,但是#{id}=null,所以显然会报错。
即:请求后面跟参数时,必须带id
5. 注意
页面的返回值,是传入的参数封装成的Employee。不是更新后的Employee。
@GetMapping("/emp" )
public Employee update(Employee employee){
Employee emp = employeeService.updateEmp(employee);
return emp;
}
@CachePut(value = "emp" , key="#result.id" )
public Employee updateEmp(Employee employee){
System.out.println("updateEmp: " + employee);
employeeMapper.updateEmp(employee);
return employee;
}
5. @CacheEvict:缓存清除
删除了一个数据后,将相应的缓存也删除掉。
注意测试时发送的请求方式:
1. @GetMapping("/delete") --- http://localhost:8080/delete?id=1
//发送的请求应该是:http://localhost:8080/delete?id=1
@GetMapping("/delete" )
public String deleteEmp(Integer id){
employeeService.deleteEmp(id);
return "success" ;
}
2. @GetMapping("/delete/{id}") --- http://localhost:8080/delete/1
//发送的请求应该是:http://localhost:8080/delete/1
@GetMapping("/delete/{id}" )
public String deleteEmp(@PathVariable("id" ) Integer id){
employeeService.deleteEmp(id);
return "success" ;
}
3. 测试结果:删除缓存之后,再次查询还是存缓存中拿到数据。
删除缓存失败
4. 原因
查询的结果放到了cacheNames = {"emp", "temp"}两个缓存中。
删除只删除了value = "emp"的缓存中的数据。
@Cacheable(cacheNames = {"emp" , "temp" })
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
@CacheEvict(value = "emp" , key="#id" )
public void deleteEmp(Integer id){
System.out.println("deleteEmp: " +id);
}
5. 重新测试一下
@CacheEvict(value = {"emp" , "temp" }, key="#id" )
public void deleteEmp(Integer id){
System.out.println("deleteEmp: " +id);
}
测试成功,删除了value = {"emp", "temp"}的缓存中对应的key的数据。
6. 属性
1. allEntries:是否删除指定缓存中的所有数据
allEntries=true,此时就不用再指定Key了。
2. beforeInvocation : 缓存的清除是否在方法之前执行
beforeInvocation=false 默认是false。
如果将其设置为true,beforeInvocation=false
可以理解为,无论方法执行如何,都必须先删除缓存。
@CacheEvict(value = {"emp" , "temp" }, key="#id" , beforeInvocation = true )
public void deleteEmp(Integer id){
System.out.println("deleteEmp: " +id);
int i =10/0;
}
先删除缓存,然后执行方法。方法有错,缓存仍然清除了。
6 @Caching
是一个组合注解
bug注意
注意:Controller中的两个方法
@GetMapping("/emp/{id}" )
public Employee getEmployee(@PathVariable("id" ) Integer id){
Employee emp = employeeService.getEmp(id);
return emp;
}
@GetMapping("emp/{lastName}" )
public Employee getEmpByLastName(@PathVariable("lastName" ) String lastName){
return employeeService.getEmpByLastNmae(lastName);
}
这样写是错误的。
因为@GetMapping("/emp/{id}")和@GetMapping("emp/{lastName}"),它们要映射的请求是一样的,
都会获取这个请求中的参数。
比如:http://localhost:8080/emp/王五
那么这俩个方法都会去匹配这个请求中的参数,所以会报一个:模糊含糊的错误
修改:
@GetMapping("/emp/{id}")
@GetMapping("emp/lastName/{lastName}")
测试:
@GetMapping("emp/lastName/{lastName}" )
public Employee getEmpByLastName(@PathVariable("lastName" ) String lastName){
return employeeService.getEmpByLastNmae(lastName);
}
@Caching(
cacheable = {
@Cacheable(value = "emp" , key = "#lastName" )
},
put = {
@CachePut(value = "emp" , key="result.id" ),
@CachePut(value = "emp" , key="#result.email" )
}
)
public Employee getEmpByLastNmae(String lastName){
return employeeMapper.getEmpByLastName(lastName);
分析一下
@Caching(
cacheable = {
@Cacheable(value = "emp" , key = "#lastName" )
},
put = {
@CachePut(value = "emp" , key="result.id" ),
@CachePut(value = "emp" , key="#result.email" )
}
)
其中的:@Cacheable(value = "emp", key = "#lastName")
先查询缓存,没有的话再执行方法。将方法的返回值存到名为emp的chche中,且key=lastName
@CachePut(value = "emp", key="result.id"),
@CachePut(value = "emp", key="#result.email")
先执行方法,再将结果存到emp缓存cache中,且key为result.id和key为result.email中。
@Cacheable你先去缓存中找, @CachePut我直接执行方法。不影响。
7 @CacheConfig能为一个类配置一个公共的cache属性
@CacheConfig(cacheNames="emp"):为整个类的所有方法都指定一个缓存cache:emp
那么方法上的:value。cacheNames等属性就不用写了。
@Cacheable(cacheNames = {"emp", "temp"})
@CachePut(value = "emp", key="#result.id")
注意:这几个注解,一定要注意是先执行缓存,还是先执行目标方法。
8. 搭建Redis环境
我们知道在没有配置时,缓存使用的是ConcurrentMapCache组件中的ConcurrentMap<Object, Object>
来做缓存容器的,它是一个Map:ConcurrentMap<K, V> extends Map<K, V>
实际在开发中,我们经常使用的是缓存中间件,比如redis,memcached...
我们前面分析了SpringBoot能开启多种的缓存配置:
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.SimpleCacheConfiguration
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
默认开启的是SimpleCacheConfiguration这个缓存配置。
其他的缓存配置在你导入相关的组件,才会生效相关的配置。
0. 整合Redis作为缓存。
1. docker启动redis
2. 连接redis
redis相关命令可以参考redis中国官网
redis中国
3. 引入redis的starter
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
RedisAutoConfiguration的自动配置就起效了。
4. 在application.yml中配置redis
redis:
host: 192.168.92.130
5. RedisAutoConfiguration向容器中注入了两个组件
1. RedisTemplate<Object, Object> : 操作所有的对象的
2. StringRedisTemplate : 专门简化操作reids中字符串的工具类
这是SpringBoot为了简化Redis操作,为我们注入的两个组件。
我们操作redis,就用这两个组件。
6. 测试一下
1. 用StringRedisTemplate操作字符串
(操作redis的5种数据类型,向其中添加/删除String类型的值)
@Test
public void test01 (){
//给redis中保存了一个String数据
stringRedisTemplate.opsForValue().append("msg" , "hello" );
String msg = stringRedisTemplate.opsForValue().get("msg" );
System.out.println(msg);
//给redis保存了List数据
stringRedisTemplate.opsForList().leftPush("mylist" , "1" );
stringRedisTemplate.opsForList().leftPush("mylist" , "2" );
}
2. 用RedisTemplate<Object, Object>操作对象
@Test
public void test02 (){
Employee empById = employeeMapper.getEmpById(1);
redisTemplate.opsForValue().set("emp01" , empById);
}
注意,这里会报错:
要求我们存入的对象是可以序列化的。
2.1 将实体类实现Serializable接口
如果保存对象,默认是使用JDK序列化机制,将序列化后的数据保存到redis中。
结果
2.2 将数据以JSON的方式保存
1. 我们可以用市面上的一些JSON转换工具尝试一下
2. redisTemplate它有默认的序列化规则
默认使用JDK的序列化规则
3. 我们想要以JSON的方式保存对象
我们可以自己指定来添加一个RedisTemplate<Object, Object>,并且指定序列化规则。
这是RedisAutoConfiguration源码中默认添加的RedisTemplate组件
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate" )
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
3.1 我们自己写一个方法
其中指明序列化采用JSON
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> serializer = new
Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(serializer);
return template;
}
}
3.2 测试
//注入我们自己的RedisTemplate
@Autowired
RedisTemplate<Object, Employee> employeeRedisTemplate;
//测试保存redis对象
@Test
public void test02 (){
Employee empById = employeeMapper.getEmpById(1);
employeeRedisTemplate.opsForValue().set("emp01" , empById);
}
9 缓存原理
默认之前用的是CocurrentMap的缓存管理器帮我们创建cache组件,缓存组件来给缓存中进行数据操作。
那么引入redis的启动器后,会发生什么样的效果呢?
1. 打开自动配置的报告
debug: true
2. 启动主程序
以前是默认开启SimpleCacheConfiguration这个配置类。
在控制台发现:RedisCacheConfiguration开启了。
RedisCacheConfiguration matched:
其他的缓存配置类都没有匹配上。
9.1 分析
1. 注入CacheManager
谁向容器中放入了缓存管理器。
@ConditionalOnMissingBean(CacheManager.class)
如果没有CacheManager.class,RedisCacheConfiguration就会注入一个CacheManager.class
我们也发现,SimpleCacheConfiguration也是当没有CacheManager.class时,
也会注入一个CacheManager.class。
但是虽然相同的规则,却又先后顺序。因为导入了Redis包,所以RedisCacheConfiguration会
先进行判断,单后就直接注入一个CacheManager。之后SimpleCacheConfiguration和其他配置类
在判断时,就不再满足这个条件了。
2. RedisCacheManager来为我们创建缓存cache
有一个createRedisCache()创建缓存。
所以RedisCacheManager帮我们创建RedisCache来作为缓存。
3. RedisCache通过操作reids来缓存数据的。
9.2 测试一下能不能缓存数据
1. 发送多次http://localhost:8080/emp/1请求
@Cacheable(cacheNames = {"emp" , "temp" })
public Employee getEmp(Integer id){
System.out.println("查询" +id+"号员工" );
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
发现第二次请求就不在交互数据库。
而且在redis中也发现了相关的缓存数据
因为我们对Employee实现了序列化接口,所以在redis中也是以JDK序列化存储。
引入redis的starter时,注入的cacheManager是RedisCacheManager
RedisCacheManager操作redis的时候使用的是RedisTemplate<Object,Object>
RedisTemplate<Object,Object>是默认使用JDK的序列化机制。
2. 将其保存为JSON
引入redis的starter时,注入的cacheManager是RedisCacheManager
RedisCacheManager操作redis的时候使用的是RedisTemplate<Object,Object>
RedisTemplate<Object,Object>是默认使用JDK的序列化机制。
因为这个原因,所以我们可以自定义一个CacheManager
3. 自定义一个CacheManager
注意
因为我的版本2.3.1和视频课1.5.1源码差别较大,无法用用视频中的方式重写cacheManager()方法。
下面方法来自评论:具体实现原理不懂。
效果:适用于所有的POJO,不是针对单独某个POJO。
序列化:向Redis中存对象时要用到序列化
反序列化:将Redis中的数据解析成对象时。
反序列化会出错:
视频课上面的实现是针对Emp这个POJO类的,也就是当我们进行查找时从缓存的取到的JSON数,要将其
转成Emp POJO类。
这个时候,当我们去查询Dept时,也会先从缓存中拿数据,然后再将其转成Dept类。但是此时的解析
规则是针对Emp类的,那么显然会出错。
比如Dept有2个属性,Emp有5个属性,那么就会报错。
SpringBoot 2.x 版本的CacheManager配置
如果写了多个CacheManager,要指定一个默认的CacheManager
@Primary //标注其是默认使用的CacheManager
//@Primary 标注其是默认使用的CacheManager,一般还是自动配置默认的
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)).
disable CachingNullValues().
serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
));
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
4. 测试报错:500
之前在Redis中的数据还没有删除,还是JDK序列化的形式。
再次查询时,发现缓存中有key对应的数据,但是这解析时按照JSON解析的。所以会报错。
5. 解决:删除掉redis的相关缓存。
测试成功。
注意我们的key多了一个前缀,会将cache的name作为key的前缀。
6. 在代码里操作Redis中的cache
@Cacheable(cacheNames = "dept" , key="#id" )
public Department getDeptById(Integer id){
System.out.println("查询部门:" +id);
Department department =departmentMapper.getDeptByid(id);
//获取某个缓存
Cache dept = cacheManager.getCache("dept" );
dept.put("dept::1" , department);
return department;
}