当前位置:   article > 正文

Redis缓存击穿、缓存穿透和缓存雪崩

Redis缓存击穿、缓存穿透和缓存雪崩

引言

在高并发的系统中,缓存是提高性能和减轻数据库压力的重要手段之一。Redis作为一种常用的缓存解决方案,被广泛应用于各种系统中。然而,当缓存使用不当或者面对特定的情况时,可能会出现缓存击穿、缓存穿透和缓存雪崩等问题。本文将详细介绍这三个问题的概念、原因和解决方法,并提供详细的Java示例代码。

1. 缓存击穿

1.1 概念

缓存击穿是指在缓存中不存在但数据库中存在的数据,当有大量并发请求同时访问这个不存在的数据时,这些请求会穿透缓存直接访问数据库,导致数据库压力过大,性能下降。

1.2 原因

缓存击穿通常发生在以下情况下:

  • 热点数据失效:某个热点数据在缓存中过期或被删除,而此时又有大量请求同时访问该数据。
  • 并发查询:多个请求同时查询一个不存在的数据,导致缓存无法命中,请求直接访问数据库。

1.3 解决方法

1.3.1 互斥锁(Mutex Lock)

使用互斥锁可以保证只有一个线程可以访问数据库,其他线程等待该线程查询数据库并将结果写入缓存。这样可以避免多个线程同时访问数据库,减轻数据库压力。

以下是使用Java的Redisson库实现互斥锁的示例代码:

  1. import org.redisson.Redisson;
  2. import org.redisson.api.RLock;
  3. import org.redisson.api.RedissonClient;
  4. import org.redisson.config.Config;
  5. public class CacheBreakdownSolution {
  6. private static final String LOCK_KEY = "lock:key";
  7. private static final String CACHE_KEY = "cache:key";
  8. public static void main(String[] args) {
  9. Config config = new Config();
  10. config.useSingleServer().setAddress("redis://localhost:6379");
  11. RedissonClient redisson = Redisson.create(config);
  12. RLock lock = redisson.getLock(LOCK_KEY);
  13. try {
  14. lock.lock();
  15. // 查询缓存
  16. Object cacheData = getFromCache(CACHE_KEY);
  17. if (cacheData == null) {
  18. // 查询数据库
  19. Object dbData = getFromDatabase();
  20. // 将数据写入缓存
  21. writeToCache(CACHE_KEY, dbData);
  22. }
  23. } finally {
  24. lock.unlock();
  25. redisson.shutdown();
  26. }
  27. }
  28. private static Object getFromCache(String key) {
  29. // 从缓存中获取数据的逻辑
  30. return null;
  31. }
  32. private static Object getFromDatabase() {
  33. // 从数据库中获取数据的逻辑
  34. return null;
  35. }
  36. private static void writeToCache(String key, Object data) {
  37. // 将数据写入缓存的逻辑
  38. }
  39. }
1.3.2 预加载(Cache Preloading)

预加载是指在缓存中设置短暂的过期时间,当缓存数据即将过期时,提前异步更新缓存。这样可以避免缓存失效时大量请求直接访问数据库。

以下是使用Java的Spring Boot和Redis实现缓存预加载的示例代码:

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cache.annotation.CacheEvict;
  5. import org.springframework.cache.annotation.Cacheable;
  6. import org.springframework.scheduling.annotation.Async;
  7. import org.springframework.scheduling.annotation.EnableAsync;
  8. import org.springframework.stereotype.Service;
  9. import java.util.concurrent.CompletableFuture;
  10. @SpringBootApplication
  11. @EnableAsync
  12. public class CacheBreakdownSolution {
  13. public static void main(String[] args) {
  14. SpringApplication.run(CacheBreakdownSolution.class, args);
  15. }
  16. @Autowired
  17. private DataService dataService;
  18. @Cacheable(value = "cache", key = "'cache:key'")
  19. public Object getData() {
  20. return dataService.getDataFromDatabase();
  21. }
  22. @Async
  23. @CacheEvict(value = "cache", key = "'cache:key'")
  24. public CompletableFuture<Void> preloadData() {
  25. dataService.preloadDataToCache();
  26. return CompletableFuture.completedFuture(null);
  27. }
  28. @Service
  29. public static class DataService {
  30. public Object getDataFromDatabase() {
  31. // 从数据库中获取数据的逻辑
  32. return null;
  33. }
  34. public void preloadDataToCache() {
  35. // 预加载数据到缓存的逻辑
  36. }
  37. }
  38. }

在上述示例中,使用Spring Boot的缓存注解@Cacheable@CacheEvict来实现缓存的读取和更新。通过异步调用preloadData()方法,可以在缓存过期前预加载数据到缓存中。

2. 缓存穿透

2.1 概念

缓存穿透是指查询一个不存在的数据,由于缓存和数据库中都不存在该数据,因此每次查询都会直接访问数据库,导致数据库压力过大。

2.2 原因

缓存穿透通常发生在以下情况下:

  • 恶意攻击:攻击者故意查询不存在的数据,以此来消耗系统资源。
  • 频繁查询不存在的数据:系统中的业务逻辑错误或设计不合理,导致频繁查询不存在的数据。

2.3 解决方法

2.3.1 布隆过滤器(Bloom Filter)

布隆过滤器是一种高效的数据结构,用于判断一个元素是否存在于集合中。它可以快速判断一个查询是否存在于缓存或数据库中,从而避免无效的查询。

以下是使用Java的Guava库实现布隆过滤器的示例代码:

  1. import com.google.common.hash.BloomFilter;
  2. import com.google.common.hash.Funnels;
  3. public class CachePenetrationSolution {
  4. private static final int EXPECTED_INSERTIONS = 1000000;
  5. private static final double FPP = 0.01;
  6. private static final BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), EXPECTED_INSERTIONS, FPP);
  7. public static void main(String[] args) {
  8. // 初始化布隆过滤器
  9. initBloomFilter();
  10. // 查询数据
  11. String data = "data";
  12. if (isDataExist(data)) {
  13. // 从缓存或数据库中获取数据
  14. Object result = getData(data);
  15. } else {
  16. // 数据不存在
  17. // ...
  18. }
  19. }
  20. private static void initBloomFilter() {
  21. // 将已有数据添加到布隆过滤器中
  22. // ...
  23. }
  24. private static boolean isDataExist(String data) {
  25. return bloomFilter.mightContain(data);
  26. }
  27. private static Object getData(String data) {
  28. // 从缓存或数据库中获取数据的逻辑
  29. return null;
  30. }
  31. }

在上述示例中,通过使用布隆过滤器,可以在查询数据之前快速判断数据是否存在于缓存或数据库中。如果数据不存在,可以避免无效的查询。

3. 缓存雪崩

3.1 概念

缓存雪崩是指缓存中的大量数据同时过期失效,导致大量请求直接访问数据库,造成数据库压力过大,甚至引起数据库宕机。

3.2 原因

缓存雪崩通常发生在以下情况下:

  • 缓存服务器宕机:缓存服务器发生故障或宕机,导致缓存失效。
  • 大量数据同时过期:由于某种原因,缓存中的大量数据在同一时间段内同时过期。
  • 数据库压力过大:当缓存失效后,大量请求直接访问数据库,造成数据库压力过大。

3.3 解决方法

3.3.1 数据过期时间随机化(Expiration Time Randomization)

通过将缓存数据的过期时间随机化,可以避免大量数据同时过期失效。将每个缓存数据的过期时间添加一个随机值,使得缓存数据的过期时间分散

在不同的时间点,从而减少缓存同时失效的概率。

以下是使用Java的Redisson库实现数据过期时间随机化的示例代码:

  1. import org.redisson.Redisson;
  2. import org.redisson.api.RBucket;
  3. import org.redisson.api.RedissonClient;
  4. import org.redisson.config.Config;
  5. import java.util.Random;
  6. import java.util.concurrent.TimeUnit;
  7. public class CacheAvalancheSolution {
  8. private static final String CACHE_KEY = "cache:key";
  9. private static final int EXPIRATION_TIME = 3600; // 缓存过期时间,单位:秒
  10. private static final int MAX_DEVIATION = 300; // 最大过期时间偏差,单位:秒
  11. public static void main(String[] args) {
  12. Config config = new Config();
  13. config.useSingleServer().setAddress("redis://localhost:6379");
  14. RedissonClient redisson = Redisson.create(config);
  15. RBucket<Object> bucket = redisson.getBucket(CACHE_KEY);
  16. // 获取缓存数据
  17. Object data = bucket.get();
  18. if (data == null) {
  19. // 查询数据库
  20. data = getDataFromDatabase();
  21. // 将数据写入缓存,并设置随机过期时间
  22. if (data != null) {
  23. int expiration = EXPIRATION_TIME + new Random().nextInt(MAX_DEVIATION);
  24. bucket.set(data, expiration, TimeUnit.SECONDS);
  25. }
  26. }
  27. redisson.shutdown();
  28. }
  29. private static Object getDataFromDatabase() {
  30. // 从数据库中获取数据的逻辑
  31. return null;
  32. }
  33. }

在上述示例中,通过生成一个随机值,将缓存数据的过期时间设置为缓存过期时间加上随机值,从而实现数据过期时间的随机化。

3.3.2 备份缓存(Cache Backup)

通过设置多级缓存架构,将缓存数据同时存储在多个缓存服务器中,以备份的方式提供数据访问。当某个缓存服务器发生故障或宕机时,可以从其他缓存服务器中获取数据,避免缓存雪崩。

以下是使用Java的Spring Boot和Redis实现缓存备份的示例代码:

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cache.annotation.Cacheable;
  5. import org.springframework.stereotype.Service;
  6. @SpringBootApplication
  7. public class CacheAvalancheSolution {
  8. public static void main(String[] args) {
  9. SpringApplication.run(CacheAvalancheSolution.class, args);
  10. }
  11. @Autowired
  12. private DataService dataService;
  13. @Cacheable(value = "primaryCache", key = "'cache:key'")
  14. public Object getData() {
  15. Object data = dataService.getDataFromCache();
  16. if (data == null) {
  17. // 从备份缓存中获取数据
  18. data = dataService.getDataFromBackupCache();
  19. }
  20. return data;
  21. }
  22. @Service
  23. public static class DataService {
  24. public Object getDataFromCache() {
  25. // 从主缓存中获取数据的逻辑
  26. return null;
  27. }
  28. public Object getDataFromBackupCache() {
  29. // 从备份缓存中获取数据的逻辑
  30. return null;
  31. }
  32. }
  33. }

在上述示例中,通过使用Spring Boot的缓存注解@Cacheable,可以先从主缓存中获取数据,如果数据不存在,则从备份缓存中获取数据。这样可以确保在主缓存失效时仍能提供数据访问。

结论

  1. 缓存穿透

    • 定义:缓存穿透是指查询一个不存在的数据,由于缓存中没有该数据,每次查询都会直接访问数据库,这样缓存就失去了意义。
    • 原因:通常是因为数据不存在,或者查询条件错误导致。
    • 解决方案:常见的解决方案包括:
      • 布隆过滤器:在缓存之前使用布隆过滤器,可以快速判断数据是否存在,从而避免对数据库的无效查询。
      • 缓存空对象:将查询结果为空的数据也缓存起来,但需要设置较短的过期时间。
      • 限制访问频率:对访问频率进行限制,防止恶意攻击。
  2. 缓存击穿

    • 定义:缓存击穿是指一个非常热点的数据在缓存过期的瞬间,大量的请求直接打到了数据库上,导致数据库压力过大。
    • 原因:通常是因为缓存中的数据过期,而此时有大量的请求需要访问这个数据。
    • 解决方案:包括:
      • 互斥锁:当缓存失效时,先判断缓存值是否存在,如果不存在,就通过互斥锁保证只有一个线程去查询数据库并回写缓存。
      • 设置随机过期时间:给缓存数据设置随机的过期时间,避免大量数据同时过期。
      • 使用分布式锁:在分布式系统中,可以使用分布式锁来保证缓存击穿时只有一个请求能够访问数据库。
  3. 缓存雪崩

    • 定义:缓存雪崩是指在缓存中的数据大规模过期或失效,导致大量的请求同时访问数据库,造成数据库压力过大甚至崩溃。
    • 原因:通常是因为缓存数据的过期时间设置不当,导致大量数据同时过期。
    • 解决方案:包括:
      • 设置不同的过期时间:为缓存数据设置不同的过期时间,避免大规模同时过期。
      • 使用分布式缓存:通过分布式缓存来分散请求压力。
      • 限流降级:在系统压力过大时,进行限流和降级处理,保证系统的可用性。

缓存击穿、缓存穿透和缓存雪崩是常见的缓存相关问题,在设计和使用缓存时需要注意避免这些问题的发生。通过使用互斥锁、预加载、布隆过滤器、数据过期时间随机化和缓存备份等解决方法,可以有效地解决这些问题,提高系统的性能和可靠性。

然而,需要根据具体的系统情况选择合适的解决方法,并进行适当的调优和测试,以确保缓存的正确使用和稳定性。

公众号请关注"果酱桑", 一起学习,一起进步!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/784592
推荐阅读
相关标签
  

闽ICP备14008679号