[springboot专栏]缓存雪崩、穿透、击穿的代码级解决方案

一、缓存使用的若干问题

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源码

本专栏是一个连贯的系列专栏,如果存在看不懂的代码,请在本专栏中按自然排序查看之前的2-3篇文章。

在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/hanxiaotongtong/article/details/122893369