今天心血了springboot的缓存机制 想把一些内容记录下来 和遇到的问题
首先Cache 缓存我们是跟mysql交互的
我们可以在MySQL建立一个数据库和表
然后我们要新建一个项目
选中web
MySQL mybatis 和I/O里面cache
接下来我们搭建基本环境
* 搭建基本环境
* 1.导入数据库文件 创建出departement 和 employee 表
* 2.创建JavaBean封装数据
* 3.整合MyBatis 操作数据库
* 1.配置数据源信息
* 2.使用注解版的MyBatis;
* 1).@MapperScan 指定需要扫描的mapper接口所在的包
* 快速体验缓存
* 步骤
* 1.开启基于注解的缓存
* 2.标注缓存注解即可
* @Cachebale
* @CacheEvict
* @CachePut
*默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache;将数据保存在 ConcurrentMap<Object, Object>中
* 开发中使用缓存中间件;redis、memcached、ehcache;
这些都是基本操作 我就只把service和controller写出来
在进行下一步的时候我们要先了解cache注解的一些功能
以EmployeeService 为例
@CacheConfig(cacheNames = "emp")
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
/**
* 将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法
*
* CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字
* 几个属性
* cacheName/value:指定的缓存组件的名字
* key:缓存数据使用的key,可以用它来指定。默认是使用方法参数的值 1-方法的返回值
* 编写SpEL #id;参数id的值 #a0 #p0 #root.args[0]
* keyGenerator:key的生成器;可以自己指定key的生成器的组件id
* key/keyGenerator:二选一使用
* CacheManager: 指定缓存管理器:或者cacheResolver指定获取解析器
* condition:制定符合条件的情况下才缓存
* condition = "#id>0"
* unless:否定缓存:当unless指定的条件为true, 方法的返回值就不会被缓存;可以获取到结果进行判断
* unless = "#result == null"
* sync:是否使用异步模式
*
*原理:
* 1.自动配置类: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.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、没有查到缓存就调用目标方法;
* 4、将目标方法返回的结果,放进缓存中
*
* @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,
* 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据;
*
* 核心:
* 1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
* 2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator
*
* 几个属性
* cacheName/value:指定的缓存组件的名字
* key:缓存数据使用的key,可以用它来指定。默认是使用方法参数的值 1-方法的返回值
* 编写SpEL #id;参数id的值 #a0 #p0 #root.args[0]
* 自定义:key = " #root.methodName+'['+#id+']' "
* keyGenerator:key的生成器;可以自己指定key的生成器的组件id
* key/keyGenerator:二选一使用
* CacheManager: 指定缓存管理器:或者cacheResolver指定获取解析器
* condition:制定符合条件的情况下才缓存
* condition = "#id>0"
* unless:否定缓存:当unless指定的条件为true, 方法的返回值就不会被缓存;可以获取到结果进行判断
* unless = "#result == null"
* sync:是否使用异步模式
* @param id
* @return
*/
@Cacheable(/*cacheNames = {"emp"}*//*,keyGenerator = "myKeyGenerator"*/)
public Employee getEmpById(Integer id){
System.out.println("查询了"+id+"号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
/**
* @CachePut 即调用方法,有更新缓存数据
* 修改数据库的某个数据,同时更新缓存
* 运行时机:
* 1.先调用目标方法
* 2.将目标方法的结果换从起来
*
* 测试步骤
* 1.查询数据库信息,查询到的结果会放在缓存中:同步更新缓存
* key:2 value :之前的结果
* 2.以后查询还是之前的结果
* 3.更新数据
* key:默认是传入的数据对象 值:返回的数据对象
* 4.查询数据
* 应该是为更新后的数据;
* key:"#employee.id",使用传入的参数的id
* key:"#result.id";使用返回值的参数的id
* @Cacheable的key是不能用#result。id:原因是查询前先根据id查询缓存中是否有数据
* 为什么是之前的而不是更新后的结果?【数据在缓冲中没有更新】
* @param employee
* @return
*/
//得先加cacheName 不然会出错
@CachePut(/*cacheNames = {"emp"},*/key = "#employee.id")
public Employee updateEmp(Employee employee){
System.out.println("修改了"+employee.getId()+"号员工");
employeeMapper.updateEmp(employee);
return employee;
}
/**
* @CacheEvict:缓存清除
* allEntries = true; 清除缓存中的所有数据(默认false)
* beforeInvocation = false;缓存的清除是否在方法执行之前
* @param id
*/
@CacheEvict(/*cacheNames = {"emp"},*/key = "#id")
public void deleteEmp(Integer id){
System.out.println("删除了"+id+"号员工");
}
@Caching(
cacheable = {
@Cacheable(/*cacheNames = "emp",*/ key = "#lastName")
},
put = {
@CachePut(/*cacheNames = "emp",*/key = "#result.id"),
@CachePut(/*cacheNames = "emp",*/key = "#result.email")
}
)
/**
* @Caching 定义复杂的缓存规则
*/
public Employee getEmpByLastName(String lastName){
return employeeMapper.getEmpByLastName(lastName);
}
}
@CacheConfig(cacheNames = “emp”)
先在类上面写这个注解的原因是这样可以使类里面的任意一个方法的缓存组件的名称都一致,这样可以减去每书写一个方法就重复一遍代码
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
几个属性
* cacheName/value:指定的缓存组件的名字
* key:缓存数据使用的key,可以用它来指定。默认是使用方法参数的值 1-方法的返回值
* 编写SpEL #id;参数id的值 #a0 #p0 #root.args[0]
* 自定义:key = " #root.methodName+'['+#id+']' "
* keyGenerator:key的生成器;可以自己指定key的生成器的组件id
* key/keyGenerator:二选一使用
* CacheManager: 指定缓存管理器:或者cacheResolver指定获取解析器
* condition:制定符合条件的情况下才缓存
* condition = "#id>0"
* unless:否定缓存:当unless指定的条件为true, 方法的返回值就不会被缓存;可以获取到结果进行判断
* unless = "#result == null"
* sync:是否使用异步模式
@Cacheable(/*cacheNames = {"emp"}*//*,keyGenerator = "myKeyGenerator"*/)
public Employee getEmpById(Integer id){
System.out.println("查询了"+id+"号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
一下使Controller类的代码
@GetMapping("/emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id){
Employee employee = employeeService.getEmpById(id);
return employee;
}
运行后
第一次查询它会进行MySQL语句的查询,这个时候我们清除一下控制台,再次点击刷新页面,就不会显示查询2号员工 这样代表没有进行mysql语句的查询,而是从缓存数据中获取信息
@CachePut 保证方法被调用,又希望结果被缓存。
//得先加cacheName 不然会出错,因为他不知道你要缓存的是哪个组件
//如果你没有加key的话 你先执行查询 然后再修改 再查询,只会得到第一次查询的数据 而修改的数据的key并不是id=#id,而是默认的使用方法名
//指定key的话可以覆盖上原先的缓存数据
* key:"#employee.id",使用传入的参数的id
* key:"#result.id";使用返回值的参数的id
* @Cacheable的key是不能用#result。id:原因是查询前先根据id查询缓存中是否有数据
@CachePut(/*cacheNames = {"emp"},*/key = "#employee.id")
public Employee updateEmp(Employee employee){
System.out.println("修改了"+employee.getId()+"号员工");
employeeMapper.updateEmp(employee);
return employee;
}
Controller的代码
@GetMapping("/emp")
public Employee updateEmp(Employee employee){
Employee employee1 = employeeService.updateEmp(employee);
return employee1;
}
自行去运行
@CacheEvict 清空缓存
这个注解更多用于删除某行数据,然后顺带将其缓存删除
/**
* @CacheEvict:缓存清除
* allEntries = true; 清除缓存中的所有数据(默认false)
* beforeInvocation = false;缓存的清除是否在方法执行之前
* @param id
*/
@CacheEvict(/*cacheNames = {"emp"},*/key = "#id")
public void deleteEmp(Integer id){
System.out.println("删除了"+id+"号员工");
}
Controller的代码
@GetMapping("/deleteEmp/{id}")
public void deleteEmp(@PathVariable("id")Integer id){
employeeService.deleteEmp(id);
}
@Caching 定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(/*cacheNames = "emp",*/ key = "#lastName")
},
put = {
@CachePut(/*cacheNames = "emp",*/key = "#result.id"),
@CachePut(/*cacheNames = "emp",*/key = "#result.email")
}
)
/**
* @Caching 定义复杂的缓存规则
*/
public Employee getEmpByLastName(String lastName){
return employeeMapper.getEmpByLastName(lastName);
}
Controller的代码
@GetMapping("/emp/lastName/{lastName}")
public Employee getEmpByLastName(@PathVariable("lastName") String lastName){
return employeeService.getEmpByLastName(lastName);
}
以上已经讲cache的部分操作内容说去完毕,接下来使用redis
我们需要一个linux虚拟机和docker来下载redis进行测试
linux虚拟机和docker来下载redis这一步请参考我之前的文章
安装完redis后我们要启动一下
docker run -d -p 6379:6379 --name 你随意取 镜像名
开启后下载redis可视化界面软件
进行一下操作,Name 你随意,Host记得填写好虚拟机的ip地址 然后点击ok就是 最后会在左上角有一个名称是你取的的redis 双击打开即可
点击右键,点击第一个选项 进入
测试一下 输入append msg hello
append 是指字符串添加
msg 是字符串的名称
hello 是字符串的内容 相当于是一个 key-value的形式
测试成功后我们就要在springboot项目上使用
1.先导入依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.在配置文件上配置redis
spring.redis.host=写你Linux的IP地址
3.去测试类测试
@Autowired
StringRedisTemplate stringRedisTemplate;//操作k-v都是字符串的
@Autowired
RedisTemplate redisTemplate;//k-v 都是对象的
/**
* Redis常见的五大数据类型
* String(字符串) stringRedisTemplate.opsForValue()
* list(列表) stringRedisTemplate.opsForList()
* set(集合) stringRedisTemplate.opsForSet()
* hash(散列) stringRedisTemplate.opsForHash()
* zSet(有序集合) stringRedisTemplate.opsForZset()
*/
@Test
public void test01(){
//给redis中保存一个数据
//stringRedisTemplate.opsForValue().append("msg","hello");
//读取数据
//System.out.println(stringRedisTemplate.opsForValue().get("msg"));
//给redis中保存一个集合 名为mylist
stringRedisTemplate.opsForList().leftPush("mylist","1");
stringRedisTemplate.opsForList().leftPush("mylist","2");
stringRedisTemplate.opsForList().leftPush("mylist","3");
stringRedisTemplate.opsForList().leftPush("mylist","4");
stringRedisTemplate.opsForList().leftPush("mylist","5");
}
每次运行后你都可以去redis的可视化界面软件查看
再进行一下操作之前,得先在我们的bean里面添加
这样才会有序列化机制
public class Employee implements Serializable
@Test
public void test02(){
Employee employee = employeeMapper.getEmpById(2);
//默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中
redisTemplate.opsForValue().set("emp01",employee);
//1.将数据以json的方式保存
//(1)自己将对象转为json
//(2)redisTemplate默认的序列化规则;改变默认的序列化规则
//EmpRedisTemplate.opsForValue().set("emp-01",employee);
}
但是运行后你去观看你的可是话界面 你会发现会出现很多你看不懂的东西,这是因为数据没有以json的方式保存到redis中
这个时候我们新建一个config类 放在config包下
@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> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(ser);
return template;
}
}
然后再test类里面引入
@Autowired
RedisTemplate<Object,Employee> EmpRedisTemplate;
然后再更改test02的代码
@Test
public void test02(){
Employee employee = employeeMapper.getEmpById(2);
//默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中
//redisTemplate.opsForValue().set("emp01",employee);
//1.将数据以json的方式保存
//(1)自己将对象转为json
//(2)redisTemplate默认的序列化规则;改变默认的序列化规则
EmpRedisTemplate.opsForValue().set("emp-01",employee);
}
测试完毕 接下来我们要进入web进行测试
先书写Department的service类和Controoler类
@Cacheable(cacheNames = "dept")
public Department getDeptById(Integer id){
System.out.println("查询部门"+id);
Department department = departmentMapper.getDeptById(id);
return department;
}
@RestController
public class DeptController {
@Autowired
DeptService deptService;
@GetMapping("/dept/{id}")
public Department getDept(@PathVariable("id") Integer id){
return deptService.getDeptById(id);
}
}
运行后可以看到查询了1号部门
但是去看了一下redis里面的内容 你会发现它并没有以json的形式保存到redis中
这是由于redis的保存默认使用它的序列化保存 所以我们应该自己自定义一个CacheManager
在config包下面的 myRedisConfig类里面书写
/**
* 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
//设置CacheManager的值序列化方式为json序列化
RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair
.fromSerializer(jsonSerializer);
RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(pair);
//设置默认超过期时间是30秒
defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
//初始化RedisCacheManager
return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
}
接下来在service里面调用
@Autowired
CacheManager cacheManager;
@Cacheable(cacheNames = "dept",cacheManager = "cacheManager")
public Department getDeptById(Integer id){
System.out.println("查询部门"+id);
Department department = departmentMapper.getDeptById(id);
return department;
}
再次运行 你就可以在redis里面看到了
ok 这次cache和redis就到这啦
如果我有什么错误或者不对的 请大家提出来 ,谢谢!!