缓存击穿

缓存击穿问题详细介绍

缓存击穿是指在高并发场景下,一个或多个请求查询一个不存在的缓存数据(通常是热点数据)可能是由于该缓存数据已过期,由于缓存中没有该数据,导致这些请求直接穿透到数据库,造成数据库的压力过大,进而导致系统性能下降的问题。

缓存击穿的主要原因包括:

  1. 缓存失效:缓存中的数据由于过期或被主动删除,导致大量请求无法从缓存中获取数据,从而直接访问数据库。
  2. 并发请求:在高并发场景下,多个进程或线程同时查询一个不存在的缓存数据,导致多个请求同时访问数据库。

缓存击穿可能带来的问题包括:

  1. 数据库压力增大:大量请求直接访问数据库,导致数据库负载增大,性能下降,甚至可能发生宕机。
  2. 响应时间增加:由于直接访问数据库需要花费更多的时间,因此缓存击穿会导致响应时间增加,降低用户体验。

解决方案

热点数据永不过期

  • 方法描述:对于频繁访问且重要的热点数据,可以设置其永不过期。同时,通过异步线程定期刷新缓存中的数据,以保持数据的实时性。
  • 优势:可以避免因缓存过期而导致的缓存击穿问题,提高系统的稳定性和性能。
  • 注意事项:需要确保定期刷新缓存的线程能够稳定运行,并且及时更新缓存中的数据以保持数据的准确性。

互斥锁

  • 方法描述:使用分布式锁或缓存锁机制,保证只有一个请求能够从数据库中加载数据,其他请求等待并使用锁中的数据。当缓存未命中时,请求会尝试获取锁,获取到锁的请求会查询数据库并更新缓存,未获取到锁的请求则等待一段时间后重试。
  • 实现方式:可以通过Redis的SETNX命令或其他分布式锁实现方式来实现。
  • 优势:可以有效减少数据库的并发访问压力,避免缓存击穿。
  • 注意事项:需要合理设置锁的等待时间和重试次数,以避免死锁或过度占用系统资源。

缓存雪崩

缓存雪崩问题介绍

缓存雪崩是指在高并发情况下,由于大量的缓存数据同时过期或缓存服务宕机,导致大量请求直接穿透到数据库,造成数据库压力过大,甚至引发系统崩溃的现象。缓存雪崩影响范围广泛,影响大于缓存击穿,严重时会导致服务不可用

缓存雪崩通常是由于以下原因导致的:

  1. 缓存集体过期:大量缓存数据被设置为同一时间过期,当这些缓存数据同时失效时,大量请求会涌入数据库。
  2. Redis服务宕机:Redis作为缓存服务,其稳定性直接影响到整个系统的性能。一旦Redis服务宕机,所有缓存数据将无法访问,所有请求都将直接打到数据库上。
  3. 大量突发请求:在某些情况下,如秒杀活动、热门事件等,可能会在短时间内产生大量的请求,这些请求如果都未能在缓存中找到数据,就会直接冲击数据库。

缓存雪崩的影响是灾难性的,它不仅会导致数据库负载过高、响应时间延长,甚至可能引发服务挂掉,影响整个系统的正常运行。

解决方案

差异化设置过期时间

  • 策略描述:在设置缓存时,避免将大量数据的过期时间设置为同一时刻。可以通过给每个键的过期时间加上一个随机偏移量,使得数据的过期时间分散开来。
  • 实施方式:在代码层面调整缓存的过期时间设置逻辑,确保不同数据的过期时间有所差异。
  • 效果:减少缓存同时失效的概率,分散数据库压力。

自定义RedisCacheManager对有效期时间进行随机设置。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 自定义CacheManager,用于设置不同的过期时间,防止雪崩问题的发生
*/
public class MyRedisCacheManager extends RedisCacheManager {
public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
//获取到原有过期时间
Duration duration = cacheConfig.getTtl();
if (ObjectUtil.isNotEmpty(duration)) {
//在原有时间上随机增加1~10分钟
Duration newDuration = duration.plusMinutes(RandomUtil.randomInt(1, 11));
cacheConfig = cacheConfig.entryTtl(newDuration);
}
return super.createRedisCache(name, cacheConfig);
}
}

使用MyRedisCacheManager

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
// 默认配置
RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 设置key的序列化方式为字符串
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value的序列化方式为json格式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues() // 不缓存null
.entryTtl(Duration.ofHours(redisTtl)); // 默认缓存数据保存1小时
// 构redis缓存管理器
// RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
// .fromConnectionFactory(redisTemplate.getConnectionFactory())
// .cacheDefaults(defaultCacheConfiguration)
// .transactionAware()
// .build();
//使用自定义缓存管理器
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
MyRedisCacheManager myRedisCacheManager = new MyRedisCacheManager(redisCacheWriter, defaultCacheConfiguration);
myRedisCacheManager.setTransactionAware(true); // 只在事务成功提交后才会进行缓存的put/evict操作
return myRedisCacheManager;
}

服务降级

  • 策略描述:在缓存失效或数据库压力过大的情况下,通过服务降级策略来减少对非核心业务的请求处理,确保核心业务的正常运行。
  • 实施方式:使用限流组件(如Hystrix、Sentinel等)来限制请求量,或返回默认数据或静态页面等。
  • 效果:保护系统不受极端情况的冲击,确保核心业务的服务质量。

使用Redis集群或哨兵模式

  • 策略描述:通过部署Redis集群或哨兵模式来提高缓存服务的可用性。当单个Redis节点故障时,可以自动切换到其他节点继续提供服务。
  • 实施方式:配置Redis集群或哨兵模式,监控Redis节点的状态,并实现故障自动转移。
  • 效果:提高缓存服务的稳定性和可靠性,降低缓存雪崩的风险。

缓存穿透

缓存穿透问题介绍

缓存穿透是指用户查询一个数据库和缓存中都不存在的数据,导致每次查询都会直接打到数据库上,而数据库中也没有该数据,如果用户不断发起这样的请求,数据库压力会非常大,甚至可能拖垮数据库。这种情况通常是由于恶意的查询或者系统设计不当导致的。

危害

缓存穿透的主要危害在于,如果存在大量针对不存在的数据的查询请求,这些请求都会绕过缓存直接访问数据库,导致数据库压力剧增,影响系统性能,甚至可能引发数据库崩溃。

解决方案

针对缓存穿透问题,常见的解决方案有以下几种:

布隆过滤器(Bloom Filter)

  • 原理:布隆过滤器是一种空间效率很高的概率型数据结构,用于判断一个元素是否在一个集合中。它通过使用位数组和多个哈希函数来减少误判率,但无法完全避免误判。

  • 应用:在查询数据库之前,先通过布隆过滤器判断该数据是否可能存在。如果布隆过滤器判断该数据不存在,则直接返回空值或错误信息,避免对数据库的访问。

  • 优缺点

    • 优点:内存占用少,查询速度快。
    • 缺点:存在误判率,且实现相对复杂。

    实现

    关于布隆过滤器的使用,建议使用Google的Guava 或 Redission基于Redis实现,前者是在单体架构下比较适合,后者更适合在分布式场景下,便于多个服务节点之间共享。

    Redission基于Redis,使用string类型数据,生成二进制数组进行存储,最大可用长度为:4294967294。

    • 引入依赖

      xml
      1
      2
      3
      4
      <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      </dependency>
    • 导入redisson配置、

      java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Configuration
      public class RedissonConfiguration {
      @Resource
      private RedisProperties redisProperties;
      @Bean
      public RedissonClient redissonSingle() {
      Config config = new Config();
      SingleServerConfig serverConfig = config.useSingleServer()
      .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
      if (null != (redisProperties.getTimeout())) {
      serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds()));
      }
      if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
      serverConfig.setPassword(redisProperties.getPassword());
      }
      return Redisson.create(config);
      }
      }
    • 自定义布隆过滤器配置

      java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      /**
      * 布隆过滤器相关配置
      */
      @Getter
      @Configuration
      public class BloomFilterConfig {
      /**
      * 名称,默认:sl-bloom-filter
      */
      @Value("${bloom.name:sl-bloom-filter}")
      private String name;
      /**
      * 布隆过滤器长度,最大支持Integer.MAX_VALUE*2,即:4294967294,默认:1千万
      */
      @Value("${bloom.expectedInsertions:10000000}")
      private long expectedInsertions;
      /**
      * 误判率,默认:0.05
      */
      @Value("${bloom.falseProbability:0.05d}")
      private double falseProbability;
    • 定义布隆过滤器接口

      java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      /**
      * 布隆过滤器服务
      */
      public interface BloomFilterService {
      /**
      * 初始化布隆过滤器
      */
      void init();
      /**
      * 向布隆过滤器中添加数据
      *
      * @param obj 待添加的数据
      * @return 是否成功
      */
      boolean add(Object obj);
      /**
      * 判断数据是否存在
      *
      * @param obj 数据
      * @return 是否存在
      */
      boolean contains(Object obj);
      }
    • 编写实现类

      java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      @Service
      public class BloomFilterServiceImpl implements BloomFilterService {
      @Resource
      private RedissonClient redissonClient;
      @Resource
      private BloomFilterConfig bloomFilterConfig;
      private RBloomFilter<Object> getBloomFilter() {
      return this.redissonClient.getBloomFilter(this.bloomFilterConfig.getName());
      }
      @Override
      @PostConstruct // spring启动后进行初始化
      public void init() {
      RBloomFilter<Object> bloomFilter = this.getBloomFilter();
      bloomFilter.tryInit(this.bloomFilterConfig.getExpectedInsertions(), this.bloomFilterConfig.getFalseProbability());
      }
      @Override
      public boolean add(Object obj) {
      return this.getBloomFilter().add(obj);
      }
      @Override
      public boolean contains(Object obj) {
      return this.getBloomFilter().contains(obj);
      }
      }

      后续在Controller直接使用布隆过滤器的contains可以直接判断该数据是否存在,从而实现过滤,在新增数据后,也需要使用add方法将数据添加到布隆过滤器,以便后面可以判断。

      局限

      由于布隆过滤器是采用hash算法将key值计算为一个数字存在bitMap中,所有有可能存在两个不同的键计算出的值相同有可能会造成误差,可以通过一个键使用多个hash算法,如果多个hash算法的结果均存在才说明该数据存在,即使这样,也会存在误差

缓存空对象

  • 原理:当查询一个不存在的数据时,将空结果(如null或特定空值)缓存起来,并设置一个较短的过期时间。这样,在后续的查询中,如果仍然查询该不存在的数据,则可以直接从缓存中返回空结果,避免对数据库的访问。
  • 应用:适用于数据变动不频繁的场景,可以减少对数据库的无效查询。
  • 优缺点:
    • 优点:实现简单,维护方便。
    • 缺点:可能会浪费一定的缓存空间,且存在短期数据不一致的风险。

控制层校验

  • 原理:在业务系统的控制层(如API接口)增加校验逻辑,对查询参数进行合法性校验。如果查询参数不合法(如查询不存在的数据ID),则直接返回错误信息,避免对缓存和数据库的访问。
  • 应用:适用于可以通过校验规则提前判断查询是否有效的场景。
  • 优缺点:
    • 优点:能够有效减少无效查询对系统的压力。
    • 缺点:需要维护校验规则,且可能无法覆盖所有无效查询。