1 简介
文档地址: https://docs.spring.io/spring-framework/docs/5.2.22.RELEASE/spring-framework-reference/integration.html#cache
-
Spring 从 3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用 JCache (JSR-107)注解简化我们开发; -
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接口下Spring 提供了各种xxxCache.的实现﹔如RedisCache,EhCacheCache ,ConcurrentMapCache 等 -
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。 -
使用Spring缓存抽象时我们需要关注以下两点
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
2 整合&体验
整合SpringCache简化缓存开发步骤:
- 引入依赖
spring-boot-starter-cache 、spring-boot-starter-data-redis - 写配置,指定缓存类型,CacheAutoConfiguration会导入RedisCacheConfiguration
- 开启缓存功能,@EnableCaching
- 使用注解完成缓存操作
主要注解操作:
- @Cacheable:触发将数据保存到缓存的操作
- @CacheEvict:触发将数据从缓存删除的操作
- @CachePut:不影响方法执行更新缓存
- @Caching:组合以上多个操作
- CacheConfig:在类级别共享缓存的配置
1、引入依赖spring-boot-starter-cache 、spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、写配置,路径:product模块下的application.properties
spring.cache.type=redis
3、在启动类上开启缓存注解@EnableCaching,路径:com/atguigu/gulimall/product/GulimallProductApplication.java
4、修改获取一级分类方法,路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java
@Cacheable("category")
@Override
public List<CategoryEntity> getLevel_1_Categorys() {
System.out.println("获取一级缓存方法执行");
List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntityList;
}
5、启动product服务测试,多次访问http://localhost:10000/,查看控制台打印信息和redis缓存。
3 @Cacheable细节配置
SpEL表达式文档:https://docs.spring.io/spring-framework/docs/5.2.22.RELEASE/spring-framework-reference/integration.html#cache-spel-context
默认的行为:
- key自动生成,格式为:缓存的名字::simplekey[]
- 缓存的value值,默认使用jdk序列化机制,将序列化后的数据存储到redis
- 默认ttl时间为-1
自定义:
- 指定生成key
- 指定缓存的ttl
- 指定数据保存为JSON格式
1、指定生成key
@Cacheable(value = {"category"}, key = "'getLevel_1_Categorys'")
@Override
public List<CategoryEntity> getLevel_1_Categorys() {
System.out.println("获取一级缓存方法执行");
List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntityList;
}
2、指定缓存的ttl
# 单位为毫秒
spring.cache.redis.time-to-live=3600000
4 自定义缓存配置
原理:
CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager-> 初始化所有的缓存 -> 每个缓存决定使用什么配置 -> 如果RedisCacheConfiguration有就用,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可 -> 就会应用到当前RedisCacheManager管理的所有缓存分区中
如果自己配置了RedisCacheConfiguration,那么在application.properties里的配置就失效了,需要将application.properties里的配置放到自己配置的RedisCacheConfiguration中开启
RedisCacheConfiguration源码:
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
ClassLoader classLoader) {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
}
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair
.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
1、编写MyCacheConfig,路径:com/atguigu/gulimall/product/config/MyCacheConfig.java
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
2、修改redis-cache配置,路径:application.properties
spring.cache.type=redis
# 单位为毫秒
spring.cache.redis.time-to-live=3600000
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
# 是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
5 @CacheEvict
@CacheEvict:失效模式
- 同时进行多种缓存操作
@Caching - 指定删除某个分区下的所有数据,
@CacheEvict(value = "category", allEntries = true) - 存储同一类型的数据,都可以指定成同一个分区
- 推荐使用默认的缓存前缀,删除
spring.cache.redis.key-prefix 的配置
1、修改getCatalogJson 方法。路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java
@Cacheable(value = "category", key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
System.out.println("查询了数据库......");
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);
Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());
List<Catelog2Vo> catelog2Vos = null;
if (level_2_categorys != null) {
catelog2Vos = level_2_categorys.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());
if (level_3_categorys != null) {
List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return collect;
}
2、修改updateDetail 方法,添加@CacheEvict注解。路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java
@CacheEvict(value = "category", allEntries = true)
@Transactional
@Override
public void updateDetail(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
3、启动服务进行测试
6 SpringCache的不足
读模式:
- 缓存穿透:查询一个null数据。解决:缓存空数据;
ache-null-values=true - 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁。SpringCache默认是无加锁的,需要加锁可以在注解
@Cacheable 上添加sync = true - 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
spring.cache.redis.time-to-live
写模式:
- 读写加锁。
- 引入Canal,感知到MysQL的更新去更新数据库
- 读多写多,直接去数据库查询就行
总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据)完全可以使用Spring-Cache,只要缓存的数据有过期时间就行
- 特殊数据:特殊处理
|