SpringCache声明式缓存
1.介绍
自Spring 3.1版本以来,Spring框架支持低侵入的方式向已有Spring应用加入缓存特性。与声明式事务类似,声明式缓存Spring Cache抽象允许一致的API来已支持多种不同的缓存解决方案,同时将对代码的影响减少到最小。
2.准备工作
0、创建一个Spring boot工程,引入redis依赖,可以完成和redis服务的交互,具体配置方式参考前文 SpringBoot与Redis7交互。
1、创建测试使用到的相关片汤代码。
实体类代码:
import java.io.Serializable;
import java.util.Date;
public class Emp implements Serializable{
private Integer empno;
private String name;
private Date birthday;
private Float salary;
private String department;
public Emp(){ //必须要有默认构造函数
}
public Emp(Integer empno, String name, Date birthday, Float salary, String department) {
this.empno = empno;
this.name = name;
this.birthday = birthday;
this.salary = salary;
this.department = department;
}
public Integer getEmpno() {
return empno;
}
public void setEmpno(Integer empno) {
this.empno = empno;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Float getSalary() {
return salary;
}
public void setSalary(Float salary) {
this.salary = salary;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
@Override
public String toString() {
return "Emp{" +
"empno=" + empno +
", name='" + name + ''' +
", birthday=" + birthday +
", salary=" + salary +
", department='" + department + ''' +
'}';
}
}
Dao代码:
import com.fblinux.sbredis.entity.Emp;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Repository
public class EmpDAO {
public Emp findById(Integer empId){
System.out.println("执行了findById方法:EmpId:" + empId);
return new Emp(empId , "fblinux" , new Date() , 1000f ,"RESEARCH");
}
public List<Emp> selectByParams(){
System.out.println("已执行selectByPrams方法");
List list = new ArrayList();
for(int i = 0 ; i < 10 ; i++) {
list.add(new Emp(i , "emp" + i , new Date() , 5000 + i * 100f , "RESEARCH"));
}
return list;
}
public Emp insert(Emp emp){
System.out.println("正在创建" + emp.getEmpno() + "员工信息");
return emp;
}
public Emp update(Emp emp){
System.out.println("正在更新" + emp.getEmpno() + "员工信息");
return emp;
}
public void delete(Integer empno){
System.out.println("删除" + empno + "员工信息");
}
}
3.使用Spring Cache
第一步:Spring Boot入口类添加EnableCaching注解,开启Spring Cache
@SpringBootApplication
public class SbredisApplication {
public static void main(String[] args) {
SpringApplication.run(SpbRedisApplication.class, args);
}
}
第二步:在业务代码层面使用@Cacheable等注解声明式的控制缓存
三个核心注解:
@ Cacheable:设置缓存。
Cacheable会将方法的返回值序列化后存储到redis中,key就是参数执行的字符串
Cacheable的用途就是在执行方法前检查对应的key是否存在,存在则直接从redis中取出不执行方法中的代码,没有对应的key则执行方法代码,并将返回值序列化保存到redis中
@ CachePut:更新缓存:可以对指定的key进行创建或者更新,通常用于更新和插入操作
@ CacheEvict:删除缓存:通常用于删除数据的过程使用
@Service
public class EmpService {
@Resource
EmpDAO empDao;
// key:根据查询id的不同,为每一个查询的不同id在redis中创建一个缓存key
//condition:代表条件成立的时候才执行缓存的数据 , 反之有一个unless ,代表条件不成立的时候才获取缓存
@Cacheable(value = "emp" , key = "#empId" ,condition = "#empId != 1000")
public Emp findById(Integer empId) {
return empDao.findById(empId);
}
@Cacheable(value = "emp:rank:salary")
public List<Emp> getEmpRank() {
return empDao.selectByParams();
}
//@CachePut 更新缓存,没有key则创建
@CachePut(value="emp" , key="#emp.empno")
public Emp create(Emp emp) {
return empDao.insert(emp);
}
@CachePut(value="emp" , key="#emp.empno")
public Emp update(Emp emp) {
return empDao.update(emp);
}
//@CacheEvict 删除缓存
@CacheEvict(value = "emp",key = "#empno")
public void delete(Integer empno) {
empDao.delete(empno);
}
}
第三步:做一个测试用例
import com.fblinux.sbredis.SbredisApplication;
import com.fblinux.sbredis.entity.Emp;
import com.fblinux.sbredis.service.EmpService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@SpringBootTest(classes = SbredisApplication.class)
public class SpringCacheTestor {
@Resource
private EmpService empService;
@Test
public void testFindById(){
Emp emp = empService.findById(1001);
emp = empService.findById(1001);
emp = empService.findById(1001);
emp = empService.findById(1001);
emp = empService.findById(1001);
emp = empService.findById(1001);
System.out.println(emp.getName());
emp = empService.findById(1000);
emp = empService.findById(1000);
emp = empService.findById(1000);
System.out.println(emp.getName());
}
@Test
public void testEmpRank() {
List list = empService.getEmpRank();
list = empService.getEmpRank();
for(Emp emp:list){
System.out.println(emp);
}
}
@Test
public void testCreate(){
empService.create(new Emp(1002 , "emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
empService.create(new Emp(1002 , "emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
empService.create(new Emp(1002 , "emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
Emp emp = empService.findById(1002);
System.out.println(emp);
}
@Test
public void testUpdate(){
empService.update(new Emp(1002 , "u-emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
empService.update(new Emp(1002 , "u-emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
empService.update(new Emp(1002 , "u-emp" + new Date().getTime() , new Date() , 1234f , "MARKET"));
Emp emp = empService.findById(1002);
System.out.println(emp);
}
@Test
public void testDelete(){
empService.delete(1002);
//Emp emp = empService.findById(1002);
//System.out.println(emp);
}
}
第四步:验证缓存是否生效
验证第一次执行testEmpRank测试方法,输出结果如下,说明执行了selectByPrams查询方法

第二次执行:可以从输出结果判断没有执行selectByPrams方法,说明数据是从redis缓存中提取出来的

验证redis中是否存入的缓存数据

验证第一次执行testFindById方法:可以看到就执行了一次findById方法,之后就一直从redis缓存中读取数据了,因为设置了缓存条件为只缓存EmpId是1000之外的数据,所以只要查询条件EmpId是1000就一直从数据库查询数据。

验证redis中的数据:为id设置了单独的key

update、delete、insert 不贴测试结果,太占篇幅了
4.使用Spring Cache的问题
在刚才使用Spring Cache的时候有三个问题
1、默认Spring Cache在redis存储数据时采用 :: 分割数据,并不是约定俗称的 冒号 分割
2、默认使用JDK序列化,正确姿势应该是应该为JSON序列化
3、默认Spring Cache注解是不支持Expire过期的,但这是日常开发中必然会用到的特性,
5.通过自定义CacheManager解决上述问题
创建应用配置类:
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class SpringCacheConfgiration {
@Bean
@Primary //设置默认的CacheManager
public CacheManager cacheManager(LettuceConnectionFactory factory){
//加载默认Spring Cache配置信息
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置有效期为1小时
config = config.entryTtl(Duration.ofHours(1));
//说明缓存Key使用单冒号进行分割
config = config.computePrefixWith(cacheName -> cacheName + ":");
//Redis Key采用String直接存储
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//Redis Value则将对象采用JSON形式存储
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//不缓存Null值对象
config = config.disableCachingNullValues();
//实例化CacheManger缓存管理器
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
//绑定REDIS连接工厂
.fromConnectionFactory(factory)
//绑定配置对象
.cacheDefaults(config)
//与声明式事务注解@Transactional进行兼容
.transactionAware()
//完成对象构建
.build();
return cacheManager;
}
// 上面key有效期配置的是一个小时,实际生产环境中不同的key需要设置不同的有效期,这里需要对这种情况单独创建一个cacheManager
@Bean
public CacheManager cacheManager1m(LettuceConnectionFactory factory){
//加载默认Spring Cache配置信息
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置有效期为1小时
config = config.entryTtl(Duration.ofMinutes(1));
//说明缓存Key使用单冒号进行分割
config = config.computePrefixWith(cacheName -> cacheName + ":");
//Redis Key采用String直接存储
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//Redis Value则将对象采用JSON形式存储
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//不缓存Null值对象
config = config.disableCachingNullValues();
//实例化CacheManger缓存管理器
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
//绑定REDIS连接工厂
.fromConnectionFactory(factory)
//绑定配置对象
.cacheDefaults(config)
//与声明式事务注解@Transactional进行兼容
.transactionAware()
//完成对象构建
.build();
return cacheManager;
}
}
测试验证:可以看到刚才的三个问题都已经解决了

针对个性化的场景设置单独的cacheManager来控制过期时间
@Cacheable(value = "emp:rank:salary" ,cacheManager = "cacheManager1m")
public List getEmpRank() {
return empDao.selectByParams();
}
执行测试方法进行验证:可以看到单独设置的cacheManager来控制过期时间生效了
