一、缓存使用的若干问题
1.1.缓存穿透
正常情况下,我们去查询数据大部分都是存在的。如果请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去,造成对后端数据库的强大压力。这种查询不存在数据的现象我们称为缓存穿透。(有可能会是某些不法份子的恶意行为,多线程打满去向服务查询不存在的数据)
解决办法
做好查询请求的数据校验,治标不治本
缓存空值,之所以会穿透缓存给压力到数据库,就是因为缓存层没有缓存null值。后文会说明在Spring Boot环境下如何配置
使用redis BloomFilter(这个已经脱离了Spring Boot课程范围,了解即可或自行学习)
1.2.缓存击穿
在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。
比如:鹿晗宣布恋情,导致微博瘫痪。就有可能是缓存击穿导致的,大家都去看这一个热点新闻,热点新闻的缓存如果超时失效了,就造成后端服务压力增大,服务器瘫痪。(当然这只是我猜的,举例而已)
解决办法
可以通过准确的监控热点流量,及时的针对热点服务及缓存组件进行自动化的扩容。
通过Hystrix或sentinel等服务限流工具,保证系统的可用性,拒绝掉一部分流量的访问。
第三种方法就是加锁,SpringCache采用sync属性,只有一个线程去维护缓存,其他线程会被阻塞,直到缓存中更新该条目为止。也就是第一次查询只允许一个线程,等数据被缓存之后,才支持并发。
@Cacheable(value = CACHE_OBJECT,key = "#id",sync=true)
public ArticleVO getArticle(Long id) {
1.3.缓存雪崩
同一时刻大量缓存失效,导致请求集中的全部打到数据库。比如:双十一零点搞活动,为了支撑这次活动,事先已经缓存好大量的数据。如果所有的数据全是缓存24小时,那24小时之后这些数据缓存将集中失效,最终结果就是11.12号服务崩溃。
解决办法
可以通过准确的监控热点流量,及时的针对热点服务及缓存组件进行自动化的扩容。
不同缓存的失效时间不能一致,同一种缓存的失效时间也尽量随机(最小值-->最大值)
二、redis 缓存配置
在 application.yml指定 spring.cache.type=redis。
spring:
cache:
type: redis
redis:
cache-null-values: true # 缓存null,防止缓存穿透
use-key-prefix: true # 是否使用缓存前缀
key-prefix: boot-launch # 缓存前缀,缓存按应用分类
time-to-live: 3600 # 缓存到期时间,默认不主动删除永远不到期
其中值得注意的一点是,Spring Cache默认只支持全局对所有的缓存配置生效时间,不支持对缓存的生效时间分类配置,容易造成缓存雪崩。
三、自定义缓存到期时间
由于redis缓存设置的到期时间是统一的,没有办法根据缓存名称(value属性)分别设置缓存到期的时间,容易造成缓存雪崩。所以我们进行一个简单的改造。在改造之前我们先来看一下RedisCacheManager源码
RedisCacheManager构造函数包含三个参数
RedisCacheWriter这个在之前的章节我们就配置过
RedisCacheConfiguration defaultCacheConfiguration 这个是默认的全局配置,针对所有缓存
Map<String, RedisCacheConfiguration> initialCacheConfigurations这个是针对某一种缓存的个性化配置,泛型String是缓存名称,泛型RedisCacheConfiguration是该缓存的个性化配置
理解了上面的源码,下面的改造代码就不难理解了。
@Data
@Configuration
@ConfigurationProperties(prefix = "caching") //application.yml配置前缀
public class RedisConfig {
//11.4章节代码,不是本节内容
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//序列化重点在这四行代码
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
//从这里开始改造
//自定义redisCacheManager
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
this.buildRedisCacheConfigurationWithTTL(redisTemplate,RedisCacheConfiguration.defaultCacheConfig().getTtl().getSeconds()), //默认的redis缓存配置
this.getRedisCacheConfigurationMap(redisTemplate)); //针对每一个cache做个性化缓存配置
return redisCacheManager;
}
//配置注入,key是缓存名称,value是缓存有效期
private Map<String,Long> ttlmap; //lombok提供getset方法
//根据ttlmap的属性装配结果,个性化RedisCacheConfiguration
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(RedisTemplate redisTemplate) {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
for(Map.Entry<String, Long> entry : ttlmap.entrySet()){
String cacheName = entry.getKey();
Long ttl = entry.getValue();
redisCacheConfigurationMap.put(cacheName,this.buildRedisCacheConfigurationWithTTL(redisTemplate,ttl));
}
return redisCacheConfigurationMap;
}
//根据传参构建缓存配置
private RedisCacheConfiguration buildRedisCacheConfigurationWithTTL(RedisTemplate redisTemplate,Long ttl){
return RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
.entryTtl(Duration.ofSeconds(ttl));
}
}
四、自定义配置实现缓存失效时间个性化
在 application.yml指定 缓存名称对应的缓存生效时间,单位为秒
caching:
ttlmap:
article: 10
xxx: 20
yyy: 50