Spring官方支持了一些配置,帮助我们操作缓存。(官方学习文档)可以帮我们简化很多操作,只需要使用几个注解,就可以帮助我们对更改的操作写入到缓存中。(本文推荐使用失效模式)
SpringCache的原理:
SpringCache里面有一个CacheManager缓存管理器,可以帮我们造出来很多缓存的组件,这些组件来进行缓存的读写。看源码:
这里创建的就是RedisCache。再来看源码中对缓存的增删改查:
@Override
protected Object lookup(Object key) {
byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
if (value == null) {
return null;
}
return deserializeCacheValue(value);
}
@Override
@SuppressWarnings("unchecked")
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;
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
*/
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
name));
}
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
*/
@Override
public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
return get(key);
}
byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
cacheConfig.getTtl());
if (result == null) {
return null;
}
return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#evict(java.lang.Object)
*/
@Override
public void evict(Object key) {
cacheWriter.remove(name, createAndConvertCacheKey(key));
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#clear()
*/
@Override
public void clear() {
byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
cacheWriter.clean(name, pattern);
}
,先介绍一下具体使用方案:
首先引入依赖:
1.引入SpringCache依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.引入redis开发场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
然后要写一下配置,springcache使用的序列化是jdk自带的序列化,会把文件转为二进制。为了更好的可读性和扩展,我们把它转为json。
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
/**
* 1.原来的配置类形式
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties {
* 因为这个并没有放到容器中,所以要让他生效 @EnableConfigurationProperties(CacheProperties.class)
* 因为这个和配置文件已经绑定生效了
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties CacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config= config.entryTtl();
config = config.serializeKeysWith(RedisSerializationContext
.SerializationPair
.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext
.SerializationPair
.fromSerializer(new GenericFastJsonRedisSerializer()));
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;
}
}
第一个注解代表绑定好配置文件和配置之间的联系,我们看一下源码:
这里中做了绑定,但是没有把其注入到spring容器中,所以我们需要指定其关系,以便从配置文件中读取到值。 所以我们还需要创建一个appliation.properties配置文件
#指定缓存类型
spring.cache.type=redis
#指定存活时间(ms)
spring.cache.redis.time-to-live=86400000
#指定前缀
spring.cache.redis.use-key-prefix=true
#是否缓存空值,可以防止缓存穿透
spring.cache.redis.cache-null-values=true
第三个注解代表开启缓存功能,这样子我们就可以使用一些注解来实现缓存操作了。
@Cacheable:把数据保存到缓存中
@CacheEvict: 将数据从缓存删除
@CachePut: 不影响方法执行更新缓存
@Caching:组合多个缓存操作
@CacheConfig: 一个类上共享缓存相同配置
实操开始:
@Cacheable(value = {"category"}, key = "'getLevel1Categorys'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys");
List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return entities;
}
当我们使用了这个注解后,一个请求过来,会去缓存中查找,如果没有则查数据库然后放到缓存中。 value指定的就是之前说的分区名字,而key就是这个缓存的key叫什么,这里使用的是SpEl表达式,所以是字符串的话要加单引号也可可以#root.MethedName直接获取方法名字。
这是Redis储存的效果 (另一个是另一个接口的暂时不管),我们清空控制台然后刷新页面再次请求就可以发现这次控制台没有打印任何东西,而是直接走的缓存。
那么如果我们要对一个数据进行修改呢怎么做?
这里使用的是失效模式,我们只需要加上这个注解,value依然是指定的分区名字,key是需要删除的缓存的key,这样当做出删除操作后,直接会对所指定的缓存数据进行删除,然后当新的请求来后就在缓存中找如果没有则进入数据库中查找并放入缓存中,如果我们要删除多个接口怎么办呢?
//删除多个缓存方式1 @Caching(evict = {
// @CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),
// @CacheEvict(value = {"category"}, key = "'getCatalogJson'")
// })
@CacheEvict(value = {"category"},allEntries = true)
public void updateCascade(CategoryEntity category) {
System.out.println("删除");
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
两种方式,一是每一个都去指定然后删除,而是用分区代表所需要删除的分区是什么,allEntries = true表示直接清空掉整个分区,这样就达到了效果。
SpringCache的不足:
1.读模式:
1.缓存穿透:查询一个null数据。解决方法:准许缓存一个空数据,在配置文件中指定即可。
2.缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方法:加锁。
3.缓存雪崩:大量的key同时过期,解决方法:加随机时间(容易弄巧成拙),加上过期时间
2.写模式:
1.读写加锁,适用于读多写少的系统。
2.引入canal,感知到mysql的更新了数据库。
3..读多写多,建议直接查数据库吧。
现在从源码角度分析,缓存击穿怎么解决:
第一个get加了本地锁Syc,虽然不不是分布式锁但是一定程度上可以起到作用,稍后我们来debug打断点看其是否起到作用。
第一次请求发现用的 是lookup方法而不是get先不管我们继续往下走,
前面都是一些判断,因为缓存为空,所以走到这里会执行业务逻辑方法,去数据库中查询数据。
然后回到这里,将我们查到的数据进行包装, 我们继续放行:
来到了RedisCache的put方法,然后整个业务流程结束。
我们发现默认是无加锁的,但那个get的方法意义呢?
这有一个属性,sync默认false,当我们改为true后看注释的解释:
将其改为true后,则会加入本地锁。我们再debug看一下:
这次直接进入到了加看本地锁的方法中, 看第一行代码,先调用第一个get方法,而这个方法就是之前的lookup方法,如果缓存中有直接返回,如果没有:valueFromLoader(key, valueLoader);从加载器中读值,放入缓存中。这里面调用了一个call方法,call方法调用的是
这个我们之前见过,就是执行本地业务代码的,然后直接返回,然后调用put方法进行保存。
我们发现只有查询是加锁,其他的都是公用的没有加锁。读模式做了处理,写模式并没有管。
总结:
常规(读多写少,及时性,一致性不高的数据完全可以用SpringCache),
如果是要求高的特殊数据需要特殊处理。