介绍
Spring 从 3.1 开始定义了 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager
接口来统一不同的缓存技术;并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache
、 EhCacheCache
、ConcurrentMapCache
等;
每次调用需要缓存功能的方法时,Spring 会检查指定参数指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点;
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
缓存管理器CacheManager
定义规则, 真正实现缓存CRUD的是缓存组件,如ConcurrentHashMap
、Redis
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- redis已经引过,此处就不写了-->
写配置
自动配置:
CacheAutoConfiguration
会导入RedisCacheConfiguration
;- 会自动装配缓存管理器
RedisCacheManager
;
手动配置:
#spring
cache:
type: redis # 使用redis作为缓存
注解
@Caching | 组合@Cachable、@CacheEvice、@CachePut |
@CacheConfig | 在类级别共享缓存的相同配置 |
测试使用缓存
开启缓存功能
启动类上添加@EnableCaching
注解
使用直接完成缓存操作
为需要使用缓存功能的方法添加@Cacheable({"category"})
注解,并指定分区(最好按照业务进行分区)
它实现的效果就是,每次调用需要缓存功能的方法时,Spring 会检查指定参数指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
默认行为
- 如果缓存中有,不会调用方法。
- key默认自动生成:缓存的名字::SimpleKey [](自主生成的key值)。
- 缓存的velue值。默认使用jdk序列化机制。将序列化后的数据存到redis
- 默认ttl时间 -1
表达式语法
自定义
-
指定生成的缓存使用的key:
key 属性,接收一个SpEL表达式
// 使用指定的值作为key @Cacheable(value = {"categories"}, key = "'level1Categories'") // 使用方法名作为key @Cacheable(value = {"categories"}, key = "#root.method.name")
-
指定缓存数据的存活时间
配置文件中修改
spring cache: redis: time-to-live: 3600000 # 单位ms
自定义缓存配置
我们希望将数据保存为json格式,需要修改SpringCache的自定义配置,这个比较麻烦,先看一下它的加载过程
原理
配置类
@EnableConfigurationProperties(CacheProperties.class) // 开启属性配置的绑定功能,否则会导致配置文件失效
@Configuration
@EnableCaching // 开启缓存功能,从启动类中移出来
public class SpringCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 序列化key
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()));
// 序列化value
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericFastJsonRedisSerializer()));
// 其它的则还是使用默认配置
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;
}
}
测试其它配置
spring
cache:
redis:
key-prefix: CACHE_ # 指定key的前缀,不指定则使用缓存的名字作为前缀
use-key-prefix: true # 是否使用前缀
cache-null-values: true # 是否缓存空值,可以防止缓存穿透
删除缓存分区的数据
分区数据
/**
* 查询一级分类
* @return
*/
@Override
@Cacheable(value = {"category"}, key = "'level1Categories'")
public List<CategoryEntity> findLevel1Categories() {
}
/**
* 查询全部分类
* 使用SpringCache缓存版本
* 只需要操作数据库,不需要关心缓存,一个注解就够了
* @return
*/
@Cacheable(value = {"category"}, key = "'categoryJson'")
@Override
public Map<Long, List<Category2VO>> getCategoryJson() {
// 查出所有分类
List<CategoryEntity> allCategories = this.list();
List<CategoryEntity> l1Categories = listByPrentCid(allCategories, 0L);
Map<Long, List<Category2VO>> categoryMap = l1Categories.stream().collect(Collectors.toMap(k1 -> k1.getCatId(), v1 -> {
List<CategoryEntity> l2Categories = listByPrentCid(allCategories, v1.getCatId());
List<Category2VO> category2VOs = null;
if (l2Categories != null && l2Categories.size() > 0) {
category2VOs = l2Categories.stream().map(l2 -> {
// 根据当前2级分类查出所有3级分类
List<CategoryEntity> l3Categories = listByPrentCid(allCategories, l2.getCatId());
List<Category3VO> category3VOs = null;
if (l3Categories != null && l3Categories.size() > 0) {
category3VOs = l3Categories.stream().map(l3 -> new Category3VO(l2.getCatId(),
l3.getCatId(), l3.getName())).collect(Collectors.toList());
}
return new Category2VO(v1.getCatId(), category3VOs, l2.getCatId(), l2.getName());
}).collect(Collectors.toList());
}
return category2VOs;
}));
return categoryMap;
}
删除一个分区的缓存数据
// 删除一个分区的缓存数据
@CacheEvict(value = "category", key = "'level1Categories'")
public void updateDetail(CategoryEntity category) {
}
删除多个分区的缓存数据
// 删除多个分区的缓存数据
@Caching(evict = {
@CacheEvict(value = "category", key = "'level1Categories'"),
@CacheEvict(value = "category", key = "'categoryJson'"),
})
删除指定分区的缓存数据
// 删除某个分区下的缓存数据,也就是清除模式
@CacheEvict(value = "category",allEntries = true)
// 如果希望修改完数据,再往缓存里放一份,也就是双写模式,可以使用这个注解
@CachePut
SpringCache原理与不足
读模式
缓存穿透
解决:保存空值
spring
cache:
redis:
cache-null-values: true
缓存击穿
解决:加锁,默认不加锁
@Cacheable(sync = true)
缓存雪崩
其实只要不是超大型并发,比如十几W的key同时过期,正好碰上十几W的请求过来查询,不用考虑这个问题
原来的解决方案是加随机时间,但是很容易弄巧成拙,假设数据不是同一时间放进去的,比如第1个数据是3秒过期,然后加了个随机数1秒,4秒过期,第2个数据是2秒过期,然后加了个随机数2秒,还是4秒过期,本来什么都不加的时候,它们过期时间不会冲撞在一起,结果有可能一加,倒还让他们撞在一起了,当时每一个数据存储的时间节点其实是不一样的,所以,只要指定过期时间就行
解决:加上过期时间
spring
cache:
redis:
time-to-live: 3600000 # 单位ms
写模式
- 读写加锁
- 引入Cannel,感知MySQL的更新去更新数据库
- 读多写多,直接查询数据库
原理
CacheManager -> Cache -> Cache负责缓存的读写
结论
普通数据
- 读多写少、及时性、一致性要求不高的数据,完全可以使用Spring Cache
- 写模式,只要缓存的数据有过期时间,就足够了
特殊数据
- 特殊设计