• Redis从入门到放弃(11):雪崩、击穿、穿透


    1、前言

    Redis作为一款高性能的缓存数据库,为许多应用提供了快速的数据访问和存储能力。然而,在使用Redis时,我们不可避免地会面对一些常见的问题,如缓存雪崩、缓存穿透和缓存击穿。本文将深入探讨这些问题的本质,以及针对这些问题的解决方案。

    2、缓存雪崩

    2.1、问题描述

    • 在某个时间点,缓存中的大量数据同时过期失效。

    • Redis宕机。

      因以上两点导致大量请求直接打到数据库,从而引发数据库压力激增,甚至崩溃的现象。

    2.2、解决方案

    1. 将 redis 中的 key 设置为永不过期,或者TTL过期时间间隔开

      import redis.clients.jedis.Jedis;
      
      public class RedisExpirationDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              // Key for caching
              String key = "my_data_key";
              String value = "cached_value";
      
              int randomExpiration = (int) (Math.random() * 60) + 1; // Random value between 1 and 60 seconds
              jedis.setex(key, randomExpiration, value);//设置过期时间
      
      		jedis.set(hotKey, hotValue);//永不过期
      
              // Retrieving data
              String cachedValue = jedis.get(key);
              System.out.println("Cached Value: " + cachedValue);
      
              // Closing the connection
              jedis.close();
          }
      }
      
    2. 使用 redis 缓存集群,实现主从集群高可用

      《Redis从入门到放弃(9):集群模式》

    3. ehcache本地缓存 + redis 缓存

      import org.ehcache.Cache;
      import org.ehcache.CacheManager;
      import org.ehcache.config.builders.CacheConfigurationBuilder;
      import org.ehcache.config.builders.CacheManagerBuilder;
      import redis.clients.jedis.Jedis;
      
      public class EhcacheRedisDemo {
          public static void main(String[] args) {
              // Configure ehcache
              CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
              cacheManager.init();
              Cache localCache = cacheManager.createCache("localCache",
                      CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class));
      
              // Configure Redis
              Jedis jedis = new Jedis("localhost", 6379);
      
              String key = "data_key";
              String value = "cached_value";
      
              // Check if data is in local cache
              String cachedValue = localCache.get(key);
              if (cachedValue != null) {
                  System.out.println("Value from local cache: " + cachedValue);
              } else {
                  // Retrieve data from Redis and cache it locally
                  cachedValue = jedis.get(key);
                  if (cachedValue != null) {
                      System.out.println("Value from Redis: " + cachedValue);
                      localCache.put(key, cachedValue);
                  } else {
                      System.out.println("Data not found.");
                  }
              }
      
              // Closing connections
              jedis.close();
              cacheManager.close();
          }
      }
      
    4. 限流降级

      限流降级需要结合其他工具和框架来实现,比如 Sentinel、Hystrix 等。

    3、缓存穿透

    3.1、问题描述

    缓存穿透指的是恶意或者非法的请求,其请求的数据在缓存和数据库中均不存在,由于大量的请求导致直接打到数据库,造成数据库负载过大。

    3.2、解决方案

    1. 使用布隆过滤器:布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于集合中。部署在Redis的前面,去拦截数据,减少对Redis的冲击,将所有可能的查询值都加入布隆过滤器,当一个查询请求到来时,先经过布隆过滤器判断是否存在于缓存中,避免不必要的数据库查询。

      import com.google.common.hash.BloomFilter;
      import com.google.common.hash.Funnels;
      import redis.clients.jedis.Jedis;
      
      public class BloomFilterDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              // Create and populate a Bloom Filter
              int expectedInsertions = 1000;
              double falsePositiveRate = 0.01;
              BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(), expectedInsertions, falsePositiveRate);
      
              String key1 = "data_key_1";
              String key2 = "data_key_2";
              String key3 = "data_key_3";
      
              bloomFilter.put(key1);
              bloomFilter.put(key2);
      
              // Check if a key exists in the Bloom Filter before querying the database
              String queryKey = key3;
              if (bloomFilter.mightContain(queryKey)) {
                  String cachedValue = jedis.get(queryKey);
                  if (cachedValue != null) {
                      System.out.println("Cached Value: " + cachedValue);
                  } else {
                      System.out.println("Data not found in cache.");
                  }
              } else {
                  System.out.println("Data not found in Bloom Filter.");
              }
      
              // Closing the connection
              jedis.close();
          }
      }
      
    2. 缓存空值:如果某个查询的结果在数据库中确实不存在,也将这个空结果缓存起来,但设置一个较短的过期时间,防止攻击者频繁请求同一不存在的数据。

      import redis.clients.jedis.Jedis;
      
      public class CacheEmptyValueDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              String emptyKey = "empty_key";
              String emptyValue = "EMPTY";
      
              // Cache an empty value with a short expiration time
              jedis.setex(emptyKey, 10, emptyValue);
      
              // Check if the key exists in the cache before querying the database
              String queryKey = "nonexistent_key";
              String cachedValue = jedis.get(queryKey);
              if (cachedValue != null) {
                  if (cachedValue.equals(emptyValue)) {
                      System.out.println("Data does not exist in the database.");
                  } else {
                      System.out.println("Cached Value: " + cachedValue);
                  }
              } else {
                  System.out.println("Data not found in cache.");
              }
      
              // Closing the connection
              jedis.close();
          }
      }
      
    3. 非法请求限制

      对非法的IP或账号进行请求限制。

      异常参数校验,如id=-1、参数空值。

    4、缓存击穿

    4.1、问题描述

    缓存击穿指的是一个查询请求针对一个在数据库中存在的数据,但由于该数据在某一时刻过期失效,导致请求直接打到数据库,引发数据库负载激增。

    4.2、解决方案

    1. 热点数据永不过期:和缓存雪崩类似,将热点数据设置为永不过期,避免核心数据在短时间内失效。
      import redis.clients.jedis.Jedis;
      
      public class HotDataNeverExpireDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              String hotKey = "hot_data_key";
              String hotValue = "hot_cached_value";
      
              // Set the hot key with no expiration
              jedis.set(hotKey, hotValue);
      
              // Retrieving hot data
              String hotCachedValue = jedis.get(hotKey);
              System.out.println("Hot Cached Value: " + hotCachedValue);
      
              // Closing the connection
              jedis.close();
          }
      }
      
    2. 使用互斥锁:在缓存失效时,使用互斥锁来防止多个线程同时请求数据库,只有一个线程可以去数据库查询数据,其他线程等待直至数据重新缓存。
      import redis.clients.jedis.Jedis;
      
      public class MutexLockDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              String mutexKey = "mutex_key";
              String mutexValue = "locked";
      
              // Try to acquire the lock
              Long lockResult = jedis.setnx(mutexKey, mutexValue);
              if (lockResult == 1) {
                  // Lock acquired, perform data regeneration here
                  System.out.println("Lock acquired. Generating cache data...");
      
                  // Simulating regeneration process
                  try {
                      Thread.sleep(5000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
      
                  // Release the lock
                  jedis.del(mutexKey);
                  System.out.println("Lock released.");
              } else {
                  System.out.println("Lock not acquired. Another thread is regenerating cache data.");
              }
      
              // Closing the connection
              jedis.close();
          }
      }
      
    3. 异步更新缓存:在缓存失效之前,先异步更新缓存中的数据,保证数据在过期之前已经得到更新。
      import redis.clients.jedis.Jedis;
      
      import java.util.concurrent.CompletableFuture;
      
      public class AsyncCacheUpdateDemo {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("localhost", 6379);
      
              String key = "data_key";
              String value = "cached_value";
      
              // Set initial cache
              jedis.setex(key, 60, value);
      
              // Simulate data update
              CompletableFuture updateFuture = CompletableFuture.runAsync(() -> {
                  try {
                      Thread.sleep(3000); // Simulate time-consuming update
                      String updatedValue = "updated_value";
                      jedis.setex(key, 60, updatedValue);
                      System.out.println("Cache updated asynchronously.");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              });
      
              // Do other work while waiting for the update
              System.out.println("Performing other work while waiting for cache update...");
      
              // Wait for the update to complete
              updateFuture.join();
      
              // Retrieve updated value
              String updatedCachedValue = jedis.get(key);
              System.out.println("Updated Cached Value: " + updatedCachedValue);
      
              // Closing the connection
              jedis.close();
          }
      }
      

    5、结论

    在使用Redis时,缓存雪崩、缓存穿透和缓存击穿是常见的问题,但通过合理的设置缓存策略、使用数据结构和锁机制,以及采用异步更新等方法,可以有效地减少甚至避免这些问题的发生。因此,在入门Redis后,不应因为这些问题而轻易放弃,而是应当深入了解并采取相应的解决方案,以充分发挥Redis在提升应用性能方面的优势。

  • 相关阅读:
    全国行政区划2023年最新版
    3.最长升序子序列 (动态规划)
    vue-element-admin分析
    Day35 贪心算法part04
    商城风格也可以很多变,DIY 了解一下
    机器学习运用-民宿价格
    软件测试测试常见分类有哪些?
    嵌入式C语言经典笔试题
    leetcode/
    git基础命令
  • 原文地址:https://www.cnblogs.com/myshare/p/17662093.html