Spring 3.1 引入了激动人心的基于annotation的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。 Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache,redis等集成。
- 通过少量的配置 annotation 就可以使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
- 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
- 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性 详细可以参考spring官方文档关于cache部分:
下面开始使用spring cache
一:导入依赖
<!--引入SpringCache 简化缓存开发-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
二:编写配置
在编写配置以前,首先得弄清楚spring cache帮我们自动配置了哪些配置,点开CacheAutoConfiguration类,其中里面有这样一段代码
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
继续点击getConfigurationClass方法,
public static String getConfigurationClass(CacheType cacheType) {
Class<?> configurationClass = MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
return configurationClass.getName();
}
看到这样一段代码:MAPPINGS.get(cacheType);,继续点进去
private static final Map<CacheType, Class<?>> MAPPINGS;
static {
Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
可以看到mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);进入RedisCacheConfiguration类中,
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return this.customizerInvoker.customize(builder.build());
}
RedisCacheManager已经被纳入容器中进行管理了。
我们首先要指定type类型,在application.properties中添加类型
三:开始测试
在测试之前,我们先看官方介绍
主要是这五个注解
@Cacheable: 触发将数据保存到缓存的操作@CacheEvict: 触发将数据从缓存删除的操作@CachePut: 会把方法的返回值put到缓存里面缓存起来,不影响方法执行更新缓存@Caching:组合以上多个操作@CacheConfig: 在类级别共享缓存的相关配置
3.1@Cacheable注解使用
3.1.1:开启缓存注解
在启动类上加上该注解
//开启使用缓存注解
@EnableCaching
3.1.2:使用@Cacheable
注意:value值也就是cacheNames是必传的,是一个list数组(可以理解成缓存分区)
@Cacheable(value={"category"})
public List<CategoryEntity> listWithTree1() {
//两种方式查询,一种是根据cat_level=1值查询,另一种是根据parent_cid=0查询
System.out.println("执行了listWithTree1()方法");
List<CategoryEntity> categoryEntities=this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", ProductConstant.CatelogLevelEnum.FIRST_LEVEL.getCode()));
return categoryEntities;
}
3.1.3启动服务测试
查看redis
可以注意到几个点
- 1)TTL为-1,也就是默认不过期
- 2)key的名字:category::SimpleKey [],(默认生成的key)
- 3)value值:默认使用jdk序列化机制,将序列化后的数据存到缓存中
- 4)如果缓存中已经有数据,则该方法不会执行
- 1)TTL为-1,也就是默认不过期 解决办法:在配置文件中设置过期时间
#统一设置过期时间,单位是毫秒
spring.cache.redis.time-to-live=36000
直接看效果
- 2)key的名字:category::SimpleKey [],(默认生成的key) 直接通过指定key来使用自己指定的key名称,注意这里的key使用的是spel表达式,官方文档对此有这样一种介绍
Available Caching SpEL Evaluation Context
方式一:
/**
* 查找所有一级分类
* @return
*/
@Cacheable(value={"category"},key = "'listWithTree1'")
public List<CategoryEntity> listWithTree1() {
//两种方式查询,一种是根据cat_level=1值查询,另一种是根据parent_cid=0查询
System.out.println("执行了listWithTree1()方法");
List<CategoryEntity> categoryEntities=this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", ProductConstant.CatelogLevelEnum.FIRST_LEVEL.getCode()));
return categoryEntities;
}
方式二:
//key使用的是spel表达式,如果指定固定名称,需要"'test'",s
@Cacheable(value={"category"},key ="#root.method.name")
public List<CategoryEntity> listWithTree1() {
//两种方式查询,一种是根据cat_level=1值查询,另一种是根据parent_cid=0查询
System.out.println("执行了listWithTree1()方法");
List<CategoryEntity> categoryEntities=this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", ProductConstant.CatelogLevelEnum.FIRST_LEVEL.getCode()));
return categoryEntities;
}
- 3)value值:默认使用jdk序列化机制,将序列化后的数据存到缓存中 最后要解决的就是让保存的value以json格式进行保存 其中RedisCacheConfiguration类有这样一段关键代码
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
SerializationPair.fromSerializer(RedisSerializer.string()),
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
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;
}
其中可以看到数据存入缓存中,key用的是RedisSerializer.string(),而value值用的是RedisSerializer.java(classLoader),如果我们想让value值用json序列化,应该怎么做呢? 编写配置类如下
/**
* 关于缓存的配置类
*/
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
RedisCacheConfiguration myCacheconfig(){
RedisCacheConfiguration config=RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return config;
}
}
测试如下
可以看到value值已经变成json格式了,但是又带来了新的问题,为什么我们在配置文件中配置得过期时间没有生效呢
原因还是看上面的代码
如果我们没配置RedisCacheConfiguration,它会读取配置文件的参数,但是如果我们写了这个RedisCacheConfiguration,底层就会直接返回RedisCacheConfiguration里的配置信息,所以配置文件看起来失效了,解决办法
直接看代码:
/**
* 关于缓存的配置类
*/
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
@Bean
RedisCacheConfiguration myCacheconfig(CacheProperties cacheProperties){
RedisCacheConfiguration config=RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties=cacheProperties.getRedis();//获取配置文件中类型为redis的所有配置
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;
}
}
其实就两步,首先加上@EnableConfigurationProperties(CacheProperties.class)注解,然后仿照源码编写即可,至此关于@Cacheable的使用初步结束,接下来我们开始使用注解CacheEvict
3.2@CacheEvict注解使用
@CacheEvict:触发将数据从缓存删除的操作
3.2.1主要代码如下
/**
* 更新分类名称,同步更新关联关系表
*/
@Transactional
@CacheEvict(value = {"category"},key = "'listWithTree1'")//删除缓存
public void updatedetail(CategoryEntity category) {
this.updateById(category);
Long catId=category.getCatId();
String name=category.getName();
categoryBrandRelationService.updateCategory(catId,name);
}
启动服务进行测试,发现当调用了updatedetail方法以后,category分区中的listWithTree1就会被删除,符合预期
3.2.2删除一个分区所有缓存
/**
* 更新分类名称,同步更新关联关系表
*/
@Transactional
@CacheEvict(value = {"category"},allEntries = true)//删除指定分区下所有缓存(注意redis并没有分区这个概念,分区只是spring往redis写入缓存的标识)
public void updatedetail(CategoryEntity category) {
//TODO 前端商品系统--》分类管理--》点击edit,数据不回显
this.updateById(category);
Long catId=category.getCatId();
String name=category.getName();
categoryBrandRelationService.updateCategory(catId,name);
}
3.3@Caching注解使用
当想使用注解一次执行多个缓存操作时,springcache也提供了这样的功能,需求是当我在后台修改数据时,我想删除listWithTree1和getCatalogJson2的缓存,应该怎么实现呢,主要代码如下:
/**
* 更新分类名称,同步更新关联关系表
*/
@Transactional
//@CacheEvict(value = {"category"},key = "'listWithTree1'")//更新的同时,删除缓存
@Caching(evict ={@CacheEvict(value = {"category"},key = "'listWithTree1'"),
@CacheEvict(value = {"category"},key = "'getCatalogJson'")})
public void updatedetail(CategoryEntity category) {
//TODO 前端商品系统--》分类管理--》点击edit,数据不回显
this.updateById(category);
Long catId=category.getCatId();
String name=category.getName();
categoryBrandRelationService.updateCategory(catId,name);
}
四:使用spring cache重构之前的方法
未使用spring cache之前的代码
public Map<String, List<Catelog2Vo>> getCatalogJson2() {
String category = redisTemplate.opsForValue().get("category");
Map<String, List<Catelog2Vo>> map=null;
if(StringUtils.isEmpty(category)){
System.out.println("未命中缓存---》");
//查询数据库
map= getCatalogJsonWithRedissonLock();
}else{
System.out.println("缓存命中了---》");
map= JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>(){
}); //内部类
}
return map;
}
改造后的代码
/**
* 使用spring cache改造getCatalogJson方法
* @return
*/
@Cacheable(value = {"category"},key ="#root.methodName" )
public Map<String,List<Catelog2Vo>> getCatalogJson(){
System.out.println("开始查询数据库---》");
//查询所有分类,用来减少数据库io
List<CategoryEntity> selectList = this.baseMapper.selectList(null);//不传查询条件查所有
//List<CategoryEntity> level1 = this.listWithTree1();
//查询所有一级分类,避免直接在数据库查询
List<CategoryEntity> level1 = getParentCid(selectList, 0L);
Map<String, List<Catelog2Vo>> map = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//查询当前一级分类对应的所有二级分类
List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
log.info("当前一级分类id是:{},对应的所有二级分类是:{}", v.getCatId(), categoryEntities);
//封装得到的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);
//根据当前二级分类找到所有的三级分类再分装成vo
List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());
log.info("当前二级分类id是:{},对应的所有三级分类是:{}", l2.getCatId(), level3Catelog);
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> collect = level3Catelog.stream().map(l3 -> {
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return map;
}
启动服务测试,符合预期(以前的加锁,写入缓存等等都不用再自己写了,我们只需要关注业务逻辑,关于缓存的操作全部使用注解来操作)
五:springcache的不足
5.1读模式场景
- 缓存穿透:查询一个null数据,
- 解决办法:缓存空数据
#设置生成null缓存,解决缓存穿透
spring.cache.redis.cache-null-values=true
- 缓存击穿:大量并发进来同时查询一个正好过期的key
- 解决办法:加锁
@Cacheable(value={"category"},key ="#root.method.name",sync = true)
Cacheable注解有个属性sync ,如果设置为true,则会在读的时候加上本地锁,RedisCache 相关源码如下(可以打断点来调试sync的值为false和true区别)
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
if (result != null) {
return (T) result.get();
}
T value = valueFromLoader(key, valueLoader);
put(key, value);
return value;
}
- 缓存雪崩:大量的key同时过期
- 解决办法:给key设置过期时间
#统一设置过期时间,单位是毫秒
spring.cache.redis.time-to-live=360000
5.2写模式场景
- 缓存一致性
- 解决办法:
- 1)读写加锁
- 2)引入canal,实现缓存和数据库的一致性
- 3)读多写多场景,直接去数据库查询 总结: 常规数据(读多写少,即时性和一致性要求不高的数据):使用springcache(只要有过期时间就可以满足大部分场景需求了) 特殊数据:特殊设计