关于Spring整合Redis我们之前已经有过介绍,这里对相关注解的使用我们就不再介绍太多,可以查看Spring整合Redis注解实现了解
JSR107缓存规范
Java Caching定义了5个核心接口
-
CachingProvider
定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期间访问多个CachingProvider
-
CacheManager
定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManage的上下文中,一个CacheManage只被一个CachingProvider拥有
-
Cache
类似于Map的数据结构并临时储存以key为索引的值,一个Cache仅仅被一个CacheManage所拥有
-
Entry
存储在Cache中的key-value对
-
Expiry
存储在Cache的条目有一个定义的有效期,一旦超过这个时间,就会设置过期的状态,过期无法被访问,更新,删除。缓存的有效期可以通过ExpiryPolicy设置。
Spring的缓存抽象
功能 | |
---|---|
Cache | 缓存接口,定义缓存操作,实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 针对方法配置,根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 update,调用,将信息更新缓存 |
@EnableCaching | 开启基于注解的缓存 |
KeyGenerator | 缓存数据时key生成的策略 |
serialize | 缓存数据时value序列化策略 |
SpringBoot整合缓存
1. 关于数据库相关配置以及Mapper、数据源的配置与编写我这里就不在赘述,注意这里的model我们要实现Serializable接口
import java.io.Serializable;
public class Department implements Serializable{
private Integer id;
private String deptName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", deptName='" + deptName + '\'' +
'}';
}
}
2. EnableCaching
在我们的项目启动类上加上@EnableCaching 注解,开启缓存
@MapperScan("com.example.cache.dao")
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
3. DepartmentServiceImpl
将方法的运行结果进行缓存,以后要是再有相同的数据,直接从缓存中获取,不用调用方法
CacheManager中管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每个缓存组件都有自己的唯一名字;
属性:
- CacheName/value:指定存储缓存组件的名字
- key:缓存数据使用的key,可以使用它来指定。默认是使用方法参数的值,1-方法的返回值
- 编写Spel表达式:#id 参数id的值, #a0/#p0 #root.args[0]
- keyGenerator:key的生成器,自己可以指定key的生成器的组件id
- key/keyGendertor二选一使用
- cacheManager指定Cache管理器,或者cacheReslover指定获取解析器
- condition:指定符合条件的情况下,才缓存;
- unless:否定缓存,unless指定的条件为true,方法的返回值就不会被缓存,可以获取到结果进行判断
- sync:是否使用异步模式,unless不支持
@Service
public class DepartmentServiceImpl implements IDepartmentService{
@Autowired
private IDepartmentDao departmentDao;
@Cacheable(key="'depts'+#root.methodName") //cacheable的key是不能使用result的参数的
@Override
public List<Department> getAll() throws Exception{
System.out.println("进来了.............");
return departmentDao.getAll();
}
}
@Cacheable
针对方法配置,根据方法的请求参数对其结果进行缓存,将方法的运行结果进行缓存,以后要是再有相同的数据,直接从缓存中获取,不用调用方法,因此cacheable的key是不能使用result的参数的(如果直接取缓存,此时是没有result的)
我们第一次运行getAll 方法,发现方法是被执行的
我们第二次运行getAll 方法,发现方法没有执行的,说明从缓存中取数据
那么问题来了,我们并没有使用redis,mogodb这种缓存中间件,springBoot是如何做缓存的呢?存在哪里呢?
原理
SpringBoot为我们自动配置了缓存相关CacheAutoConfiguration,并且引入了很多的缓存组件
在我们没有配置任何缓存中间件的时候也就是没有导入任何相关缓存启动器,默认SimpleCacheConfiguration生效
给容器注册一个CacheManager:ConcurrentMapCacheManager
可以获取和创建ConcurrentMapCache,作用是将数据保存在ConcurrentMap中
这也就是我们没有配置任何缓存中间件就可以使用缓存的原因了!
运行流程
1、方法运行之前,先查Cache(缓存组件),按照cacheName的指定名字获取;
(CacheManager先获取相应的缓存),第一次获取缓存如果没有cache组件会自己创建
2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;
key是按照某种策略生成的,默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key
没有参数 key=new SimpleKey()
如果有一个参数 key=参数值
如果多个参数 key=new SimpleKey(params);
3、没有查到缓存就调用目标方法
4、将目标方法返回的结果,放回缓存中
方法执行之前,@Cacheable先来检查缓存中是否有数据,按照参数的值作为key去查询缓存,如果没有,就运行方法,存入缓存,如果有数据,就取出map的值。
SpringBoot整合Redis
关于Redis这里就不做过多的介绍了,我们直接开始整合
- 加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件配置主机地址
spring.redis.host=127.0.0.1
SpringBoot会自动帮我们配置RedisTemplate 和 StringRedisTemplate
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis() throws Exception{
stringRedisTemplate.opsForList().leftPush("testString","hello");
stringRedisTemplate.opsForList().leftPush("testString","world");
System.out.println(stringRedisTemplate.opsForList().leftPop("testString"));
System.out.println(stringRedisTemplate.opsForList().leftPop("testString"));
Department department = departmentDao.getOne(19);
redisTemplate.opsForList().leftPush("departments",department);
}
可以看到字符串已经正常出队,而部门数据也缓存在Redis
接下来我们使用Redis完成我们部门的增删改差功能(一个List的缓存以及每个部门的缓存)
/**
* CacheConfig抽取缓存的公共配置
* 然后下面的value=department就不用写了
*/
@CacheConfig(cacheNames="department")
@Service
public class DepartmentServiceImpl implements IDepartmentService{
@Autowired
private IDepartmentDao departmentDao;
/**
* List查询
* @return
* @throws Exception
*/
@Cacheable(key="'depts'+#root.methodName") //cacheable的key是不能使用result的参数的
@Override
public List<Department> getAll() throws Exception{
System.out.println("进来了.............");
return departmentDao.getAll();
}
/**
* 在添加的时候我们要清除list缓存并更新改记录缓存
* 注意因为我们key使用id作为其中一部分,所以我们添加之后要把主键映射到dept对象上
* @param dept
* @return
* @throws Exception
*/
@Caching(put={
@CachePut(key = "'yangzhao-'+#dept.id")
},evict = {
@CacheEvict(key="'deptsgetAll'")//可以指定先删除缓存还是先删除数据库
})
@Override
public Department insert(Department dept) throws Exception {
return 1 == departmentDao.insert(dept)?dept:null;
}
/**
* 在修改的时候我们要清除list缓存并更新改记录缓存
* @param dept
* @return
* @throws Exception
*/
@Caching(put={
@CachePut(key = "'yangzhao-'+#dept.id")//先执行方法 再缓存
},evict = {
@CacheEvict(key="'deptsgetAll'")
})
@Override
public Department update(Department dept) throws Exception {
departmentDao.update(dept);
return dept;
}
/**
* 在删除的时候我们要清空list缓存以及该对象缓存,对此项目可以使用allEntries
* 可以使用自定义keyGenerator
* @param id
* @throws Exception
*/
@Caching(evict={
@CacheEvict(keyGenerator = "myKeyGenerator",beforeInvocation = true),//方法执行前清空缓存 allEntries = true清空所有
@CacheEvict(key="'depts'+'getAll'")
})
// @CacheEvict(allEntries = true)
@Override
public void delete(Integer id) throws Exception {
departmentDao.delete(id);
}
/**
* Id查询(如果Id大于16才缓存)
* @param id
* @return
* @throws Exception
*/
@Cacheable(keyGenerator = "myKeyGenerator",condition = "#id > 16") //满足条件才缓存 与 unless 相反 有缓存走缓存 没有走方法
@Override
public Department get(Integer id) throws Exception {
System.out.println("get............");
return departmentDao.getOne(id);
}
}
MyKeyGenerator
@Component
public class MyKeyGenerator implements KeyGenerator
{
@Override
public Object generate(Object target, Method method, Object... params) {
System.out.println("参数="+params[0]);
System.out.println("参数="+ Arrays.asList(params));
return "yangzhao-"+params[0];
}
}
IDepartmentDao
@Insert({"insert into department (dept_name) values(#{deptName})"})
@SelectKey(keyProperty="id",before=false,statement="SELECT LAST_INSERT_ID()",resultType=int.class,keyColumn="id")
public int insert(Department department) throws Exception;
测试添加
@Test
public void add() throws Exception{
Department dept = new Department();
dept.setDeptName("测试添加");
departmentService.insert(dept);
}
可以看到 Redis中已经多了一个缓存,并且List缓存已经被清空
@Test
public void getOne() throws Exception{
System.out.println(departmentService.get(32));
}
运行get方法并没有打印get…说明从缓存读取数据
但是确是十六进制不方便查看
以json方式传输对象
新建一个Redis的配置类MyRedisConfig
@Configuration
public class MyRedisConfig {
@Bean
RedisTemplate<Object,Department> deptRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Department> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Department.class);
redisTemplate.setDefaultSerializer(serializer);
return redisTemplate;
}
@Primary // 有多个cacheManager需要制定一个默认的一般将jdk的cacheManager作为默认
@Bean
RedisCacheManager deptRedisCacheManager(RedisTemplate<Object,Department> deptRedisTemplate){
RedisCacheManager manager = new RedisCacheManager(deptRedisTemplate);
manager.setUsePrefix(true);// 使用前缀,默认将CacheName作为前缀
return manager;
}
@Bean
RedisTemplate<Object,Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Employee> employeeRedisTemplate = new RedisTemplate<>();
employeeRedisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Employee.class);
employeeRedisTemplate.setDefaultSerializer(serializer);
return employeeRedisTemplate;
}
@Bean
RedisCacheManager empRedisCacheManager(RedisTemplate<Object,Employee> employeeRedisTemplate){
RedisCacheManager empRedisCacheManager = new RedisCacheManager(employeeRedisTemplate);
empRedisCacheManager.setUsePrefix(true);
return empRedisCacheManager;
}
}
然后在我们的DepartmentServiceImpl指定
@CacheConfig(cacheNames="department",cacheManager = "deptRedisCacheManager")
@Service
public class DepartmentServiceImpl implements IDepartmentService{
再次运行添加方法,可以看已经是已json方式保存了
测试getAll
可以看到第一次访问数据库且Redis中也将查询结果做了缓存
测试删除
@Test
public void del() throws Exception{
departmentService.delete(32);
}
删除前缓存情况
删除后缓存情况
测试修改
@Test
public void update() throws Exception{
Department dept = new Department();
dept.setDeptName("测试修改");
dept.setId(31);
departmentService.update(dept);
}
修改前
修改后
总结
- 使用缓存时,默认使用的是ConcurrentMapCache,将数据保存在ConcurrentMap中,开发中使用的是缓存中间件,redis、memcached、ehcache等
- starter启动时,有顺序,redis优先级比ConcurrentMapCache更高,CacheManager变为RedisCacheManager,所以使用的是redis缓存
- 传入的是RedisTemplate<Object, Object> 默认使用的是jdk的序列化保存, 我们可以自定义CacheManager,编写一个配置类
- XXXRedisTemplate只能用来反序列化XXX,而不能反序列化其他对象,因此要创建不同的XXXRedisTemplate 并且分别创建XXXManager