Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。
Spring Boot继承了Spring框架的缓存管理功能,通过使用 @EnableCaching 注解开启基于注解的缓存支持,Spring Boot就可以启动缓存管理的自动化配置。
基础环境搭建:
Spring boot版本:2.7.6
持久层:Spring Data Jpa
数据库:mysql 5.7
redis:latest
maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.properties
# MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
#显示使用JPA进行数据库查询的SQL语句
spring.jpa.show-sql=true
#开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true
#解决乱码
server.servlet.encoding.force-response=true
pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "tb_resume")
public class Resume {
/**
* GenerationType.SEQUENCE:依靠序列来产⽣主键 Oracle
* GenerationType.IDENTITY:依赖数据库中主键⾃增功能 Mysql
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "address")
private String address;
@Column(name = "name")
private String name;
@Column(name = "phone")
private String phone;
}
repository
public interface ResumeRepository extends JpaRepository<Resume,Long> {
//根据id修改人物
@Query(value = "update t_resume t set t.name = ?1 where t.id = ?2",nativeQuery = true)
int updateResume(String name,Long id);
}
service
@Service
public class ResumeService {
@Autowired
private ResumeRepository repository;
public Resume getResumeById(Long id){
Resume resume = repository.findById(id).get();
System.out.println(resume);
return resume;
}
}
controller
@RestController
public class ResumeController {
@Autowired
private ResumeService service;
@RequestMapping("/getResumeById")
public Resume getResumeById(Long id){
Resume resume = service.getResumeById(id);
return resume;
}
}
测试结果
Hibernate: select resume0_.id as id1_0_0_, resume0_.address as address2_0_0_, resume0_.name as name3_0_0_, resume0_.phone as phone4_0_0_ from tb_resume resume0_ where resume0_.id=?
Resume(id=1, address=北京, name=刘二, phone=010100000)
改造Spring boot默认缓存
在前面搭建的Web应用基础上,开启Spring Boot默认支持的缓存,体验Spring Boot默认缓存的使用效果
使用 @EnableCaching 注解开启基于注解的缓存支持
@EnableCaching //开启spring基于注解缓存管理支持
@SpringBootApplication
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
使用 @Cacheable 注解对数据操作方法进行缓存管理。将 @Cacheable 注解标注在Service类的查 询方法上,对查询结果进行缓存
@Service
public class ResumeService {
@Autowired
private ResumeRepository repository;
//@Cacheable 该注解作用就是将查询结果存放到springboot默认缓存中
//cacheNames 起一个缓存命名空间 对应缓存唯一标识
@Cacheable(cacheNames = "resume")
public Resume getResumeById(Long id){
Resume resume = repository.findById(id).get();
System.out.println(resume);
return resume;
}
}
测试结果,除了第一次点击后台进行数据库访问外,后面的每一次调用都是直接从Springboot缓存中获取数据。
简单分析一下原理:
-
底层结构:在诸多的缓存自动配置类中, SpringBoot默认装配的是 SimpleCacheConfiguration 他使用的 CacheManager 是 ConcurrentMapCacheManager ,使用 ConcurrentMap 当底层的数据 结构,按照Cache的名字查询出Cache, 每一个Cache中存在多个k-v键值对,缓存值
@EnableCaching注解解析
@EnableCaching 是由spring框架提供的,Springboot 框架对该注解进行了继承,该注解需要配置在类上(通常配置在项目启动类上),用于开启基于注解的缓存支持
@Cacheable注解解析
@Cacheable 注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于 对方法结果进行缓存存储。注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果 进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据
@Cacheable 注解提供了多个属性,用于对缓存存储进行相关配置
属性名 | 说明 |
---|---|
value/cacheNames | 指定缓存空间的名称,必配属性。这两个属性二选一使用 |
key | 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
keyGenerator | 指定缓存数据的key的生成器,与key属性二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,与cacheManager属性二选一使用 |
condition | 指定在符合某条件下,进行数据缓存 |
unless | 指定在符合某条件下,不进行数据缓存 |
sync | 指定是否使用异步缓存。默认false |
执行流程&时机
方法运行之前,先去查询Cache(缓存组件),按照 cacheNames 指定的名字获取,( CacheManager 先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建;
去Cache中查找缓存的内容,使用一个key,默认就是方法的参数,如果多个参数或者没有参数,是按 照某种策略生成的,默认是使用 KeyGenerator 生成的,使用 SimpleKeyGenerator 生成 key, SimpleKeyGenerator 生成 key 的默认策略:
参数个数 | key |
---|---|
没有参数 | new SimpleKey() |
有一个参数 | 参数值 |
多个参数 | new SimpleKey(params) (params:所有参数集合) |
常用的SPEL表达式
描述 | 示例 |
---|---|
当前被调用的方法名 | #root.mathodName |
当前被调用的方法 | #root.mathod |
当前被调用的目标对象 | #root.target |
当前被调用的目标对象类 | #root.targetClass |
当前被调用的方法的参数列表 | #root.args[0] 第一个参数, #root.args[1] 第二个参数... |
根据参数名字取出值 | #参数名, 也可以使用 #p0 #a0 0是参数的下标索引 |
当前方法的返回值 | #result |
@CachePut注解
目标方法执行完之后生效, @CachePut 被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个 注解保证这个方法依然会执行,执行之后的结果被保存在缓存中。
@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。
更新操作,前端会把 id+ 实体传递到后端使用,我们就直接指定方法的返回值从新存进缓存时的 key="#id" , 如果前端只是给了实体,我们就使用 key="#实体.id" 获取 key. 同时,他的执行时机是目标 方法结束后执行, 所以也可以使用 key="#result.id" , 拿出返回值的id;
@CacheEvict注解
@CacheEvict 注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解 的作用是删除缓存数据。@CacheEvict 注解的默认执行顺序是,先进行方法调用,然后将缓存进行清 除。
Spring Boot整合Redis缓存实现
在Spring Boot默认缓存管理的基础上引入Redis 缓存组件,使用基于注解的方式整合Redis缓存的具体实现;
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
当我们添加进 redis 相关的启动器之后, SpringBoot 会使用 RedisCacheConfigratioin 当做生效的自动配置类进行缓存相关的自动装配,容器中使用的缓存管理器是 RedisCacheManager , 这个缓存管理器创建的 Cache 为 RedisCache , 进而操控 redis 进行数据的缓存
Redis服务连接配置
# Redis服务地址
spring.redis.host=192.168.192.200
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=wsy@123
Service进行改造,使用@Cacheable、@CachePut、@CacheEvict三个注 解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除
@Service
public class ResumeService {
@Autowired
private ResumeRepository repository;
/**
* select
* @param id
* @return
*/
//@Cacheable 该注解作用就是将查询结果存放到springboot默认缓存中
//cacheNames 起一个缓存命名空间 对应缓存唯一标识
//value 缓存结果,key:默认在只有一个参数的情况下,key值默认就是方法参数值,如果没有参数或者多个参数的情况下,由SimpleKeyGenerator生成key
//unless 指定在符合某条件下,不进行数据缓存
@Cacheable(cacheNames = "resume",unless = "#result==null")
public Resume getResumeById(Long id){
Resume resume = repository.findById(id).get();
System.out.println(resume);
return resume;
}
/**
* update
* @param resume
* @return
*/
@CachePut(cacheNames = "resume",key = "#result.id")
public Resume updataResume(Resume resume){
repository.updateResume(resume.getName(), resume.getId());
return resume;
}
/**
* delete
* @param id
*/
@CacheEvict(cacheNames = "resume")
public void deleteResume(Long id){
repository.deleteById(id);
}
}
controller
@RestController
public class ResumeController {
@Autowired
private ResumeService service;
@RequestMapping("/getResumeById")
public Resume getResumeById(Long id){
Resume resume = service.getResumeById(id);
return resume;
}
@RequestMapping("/updateResume")
public Resume updateResume(Resume resume){
Resume resume1 = service.getResumeById(resume.getId());
resume1.setName(resume.getName());
Resume resume2 = service.updataResume(resume1);
return resume2;
}
@RequestMapping("/deleteResume")
public void deleteResume(Long id){
service.deleteResume(id);
}
}
运行getResumeById 测试结果:
运动deleteResume 测试结果:
基于API的Redis缓存实现
ApiResumeService
@Service
public class ApiResumeService {
@Autowired
private ResumeRepository repository;
@Autowired
private RedisTemplate redisTemplate;
/**
* select
* @param id
* @return
*/
public Resume getResumeById(Long id){
Object o = redisTemplate.opsForValue().get("resume_" + id);
if(o != null) {
return (Resume) o;
} else {
Resume resume = repository.findById(id).get();
//将查询结果存到缓存中,同时还可以设置有效期为1天
redisTemplate.opsForValue().set("resume_" + id,resume,1, TimeUnit.DAYS);
System.out.println(resume);
return resume;
}
}
/**
* update
* @param resume
* @return
*/
public Resume updataResume(Resume resume){
repository.updateResume(resume.getName(), resume.getId());
redisTemplate.opsForValue().set("resume_" + resume.getId(),resume);
return resume;
}
/**
* delete
* @param id
*/
public void deleteResume(Long id){
repository.deleteById(id);
redisTemplate.delete("resume_"+id);
}
}
ApiResumeController
@RestController
@RequestMapping("/api")
public class ApiResumeController {
@Autowired
private ApiResumeService service;
@RequestMapping("/getResumeById")
public Resume getResumeById(Long id){
Resume resume = service.getResumeById(id);
return resume;
}
@RequestMapping("/updateResume")
public Resume updateResume(Resume resume){
Resume resume1 = service.getResumeById(resume.getId());
resume1.setName(resume.getName());
Resume resume2 = service.updataResume(resume1);
return resume2;
}
@RequestMapping("/deleteResume")
public void deleteResume(Long id){
service.deleteResume(id);
}
}
测试结果与上面无意;
自定义Redis缓存序列化机制
-
Redis API默认序列化机制
基于API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打开 RedisTemplate类,查看该类的源码信息
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware { // 声明了key、value的各种序列化方式,初始值为空 private boolean enableTransactionSupport = false; private boolean exposeConnection = false; private boolean initialized = false; private boolean enableDefaultSerializer = true; @Nullable private RedisSerializer<?> defaultSerializer; @Nullable private ClassLoader classLoader; @Nullable private RedisSerializer keySerializer = null; @Nullable private RedisSerializer valueSerializer = null; @Nullable private RedisSerializer hashKeySerializer = null; @Nullable private RedisSerializer hashValueSerializer = null; private RedisSerializer<String> stringSerializer = RedisSerializer.string(); @Nullable private ScriptExecutor<K> scriptExecutor; private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this); private final ListOperations<K, V> listOps = new DefaultListOperations(this); private final SetOperations<K, V> setOps = new DefaultSetOperations(this); private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this, ObjectHashMapper.getSharedInstance()); private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this); private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this); private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this); private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this); public RedisTemplate() { } // 进行默认序列化方式设置,设置为JDK序列化方式 public void afterPropertiesSet() { super.afterPropertiesSet(); boolean defaultUsed = false; if (this.defaultSerializer == null) { this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader()); } 。。。。。。
从上述 RedisTemplate 核心源码可以看出,在 RedisTemplate 内部声明了缓存数据key、value的各 种序列化方式,且初始值都为空;在afterPropertiesSet() 方法中,判断如果默认序列化参数 defaultSerializer 为空,将数据的默认序列化方式设置为 JdkSerializationRedisSerializer
根据上述源码信息的分析,可以得到以下两个重要的结论:
1, 使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接 口(例如Serializable);
2, 使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式。
-
自定义RedisTemplate序列化机制
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开 RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式
@AutoConfiguration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } ......
从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂 RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了 @ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自 定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate不会生效。
如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建 一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可,如下:
@Configuration public class RedisConfig { //自定义 RedisTemplate @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); //创建Json格式序列化对象,对缓存数据的key和value进行转换 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // 解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); //设置 RedisTemplate 模板Api的序列化方式为Json template.setDefaultSerializer(jackson2JsonRedisSerializer); return template; } }
-
测试结果: