使用springcache做缓存,需要首先明白几个概念:缓存穿透、缓存击穿、缓存雪崩,以及缓存一致性。
缓存一致性解决方案——改数据时如何保证缓存和数据库中数据的一致性
从查询数据库性能优化谈到redis缓存-谈一谈缓存的穿透、雪崩、击穿
Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
Cache:缓存接口,定义缓存操作。实现有RedisCache、EhCacheCache、ConcurrentMapCache等。
CacheManager:缓存管理器,管理各种缓存(Cache)组件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 引入redis,默认的letture有内存溢出的bug,这里用jedis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
(1)自动配置
CacheAutoConfiguration自动配置类(会导入RedisCacheConfiguration),
RedisCacheConfiguration自动配好了RedisCacheManager缓存管理器,
RedisCacheManager初始化所有的缓存,每个缓存决定使用什么配置,如果RedisCacheConfiguration有就用已有的,没有就用redis的配置文件中的配置,或者默认的配置defaultCacheConfig。
想修改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可,就会应用到当前的RedisCacheManager缓存管理器管理的所有缓存分区中。
(2)我们需要做的配置
spring.cache.type=redis
#使用默认的即可
#spring.cache.cache-names=qq
#毫秒为单位,缓存过期时间
spring.cache.redis.time-to-live=3600000
#如果指定了key前缀就用我们指定的前缀,如果没有就默认使用缓存的名字(value)作为前缀,最好不指定前缀
#spring.cache.redis.key-prefix=CACHE_
#是否使用前缀,默认为true,不写即可
#spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
(1)开启缓存功能
在配置类加上注解:
@EnableCaching //开启缓存功能
(2)只需要使用注解就能使用缓存操作
@Cacheable:触发将数据保存到缓存的操作
@CacheEvict:触发将数据从缓存删除的操作
@CachePut:不影响方法执行更新缓存
@Caching:组合以上多个操作
@CacheConfig:在类级别共享缓存的相同配置
// 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
@Cacheable({"category"})
public Category getCategory(){
springcache默认就是缓存null值的,默认就是解决缓存穿透的。
我们也可以在配置文件中加入该配置,让其缓存null值。
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
@Cacheable(value = {"category"})
public List<CategoryEntity> getLevel1Categorys() {
@Cacheable的value属性,可以指定缓存的分区,这个分区category就是缓存key 的前缀,作为key的其中一部分。
缓存的分区很重要,后续删除缓存时,可以按照分区删除,最好是按照业务进行分区,这样数据有修改的话,可以将整个业务的缓存都删掉,处理起来更方便。
springcache在redis存储的key是可以设置的。
@Cacheable(value = {"category"},key = "#root.method.name")
public List<CategoryEntity> getLevel1Categorys() {
默认的key的存储的值是自动生成的:缓存的名字::SimpleKey::[] (自动生成key值)
其中缓存的名字就是分区category,也可以在配置文件中指定key的前缀(最好是不指定,使用分区前缀即可,方便区分)
#如果指定了key前缀就用我们指定的前缀,如果没有就默认使用缓存的名字(value)作为前缀,最好不指定前缀
#spring.cache.redis.key-prefix=CACHE_
#是否使用前缀,默认为true,不写即可
#spring.cache.redis.use-key-prefix=true
指定了@Cacheable的key属性之后,就会将key的后缀替换为key属性。
其中@Cacheable的key属性是可以使用spel表达式的,具体使用如下:
默认过期时间为-1,也就是永久,可以在配置文件中指定过期时间
#毫秒为单位,缓存过期时间
spring.cache.redis.time-to-live=3600000
默认是使用jdk的序列化器,存储的是jdk序列化后的数据,我们如果想要保存为json数据可以自己来设置配置类
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
/**
* 用上配置文件的配置
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 使用json序列化器,用fastjson的也可以
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;
}
}
加上sync = true属性,默认是false。
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
public List<CategoryEntity> getLevel1Categorys() {
我们正常解决缓存击穿是使用分布式锁来对数据库的操作加一把分布式锁,springcache解决的方式是使用synchronize加本地锁,虽然不能锁全部服务器,但是锁住当前服务器也是可以大大减少了并发量,多查几次数据库其实也没什么问题,只要不是大量请求来查数据库就可以。
springcache只有查询的时候才有锁,解决缓存击穿的问题,是加了synchronize本地锁,并不是分布式锁。
缓存一致性解决方案——改数据时如何保证缓存和数据库中数据的一致性
缓存更新就涉及缓存一致性问题。
/**
* 级联更新所有关联的数据
*
* @CacheEvict:失效模式(此处使用的是失效模式)
* @CachePut:双写模式,需要有返回值(用起来不方便)
* 1、同时进行多种缓存操作:@Caching
* 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
* 3、由2可知,存储同一类型的数据,都可以指定为同一分区,方便删除数据
* @param category
*/
// @Caching(evict = {
// @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
// @CacheEvict(value = "category",key = "'getCatalogJson'")
// })
@CacheEvict(value = "category",allEntries = true) //删除某个分区下的所有数据
public void updateCascade(CategoryEntity category) {
// 方法执行完之后,springcache进行的操作:
//同时修改缓存中的数据
//删除缓存,等待下一次主动查询进行更新
}
springcache是做不到缓存与数据库强一致性的问题的,我们可以使用特殊手段来做到强一致性(例如:在方法调用之前加锁,然后再调用目标方法)。
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)。
特殊数据需要进行特殊设计。
springcache极大的简化了我们对缓存的操作,常规数据完全可以使用springcache。
但是,springcache无法保证缓存与数据库的强一致性,如果对数据实时性要求不是很高的话,可以使用其失效模式,缓存过期后自动刷新最新的数据。
如果是对缓存与数据一致性要求高的话,就需要考虑自己来设计,加上锁了。