Redis從入門到放棄(11):雪崩、擊穿、穿透

2023-08-28 15:04:08

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<String, String> 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<String> 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<Void> 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在提升應用效能方面的優勢。