21. 缓存雪崩/击穿/穿透
1、前言
Redis作为一款高性能的缓存数据库,为许多应用提供了快速的数据访问和存储能力。然而,在使用Redis时,我们不可避免地会面对一些常见的问题,如缓存雪崩、缓存穿透和缓存击穿。本文将深入探讨这些问题的本质,以及针对这些问题的解决方案。
缓存(Cache)的作用是减少服务器对数据源的访问频率,从而提高数据库的稳定性。访问的流程如下。
流程图
2、缓存雪崩
2.1、问题描述
在某个时间点,缓存中的大量数据同时过期失效。
Redis宕机。
因以上两点导致大量请求直接打到数据库,从而引发数据库压力激增,甚至崩溃的现象。
2.2、解决方案
将 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(); } }
使用 redis 缓存集群,实现主从集群高可用
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(); } }
限流降级
限流降级需要结合其他工具和框架来实现,比如 Sentinel、Hystrix 等。
3、缓存穿透
3.1、问题描述
缓存穿透指的是恶意或者非法的请求,其请求的数据在缓存和数据库中均不存在,由于大量的请求导致直接打到数据库,造成数据库负载过大。
3.2、解决方案
使用布隆过滤器:布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于集合中。部署在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(); } }
缓存空值:如果某个查询的结果在数据库中确实不存在,也将这个空结果缓存起来,但设置一个较短的过期时间,防止攻击者频繁请求同一不存在的数据。
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(); } }
非法请求限制
对非法的IP或账号进行请求限制。
异常参数校验,如id=-1、参数空值。
4、缓存击穿
4.1、问题描述
缓存击穿指的是一个查询请求针对一个在数据库中存在的数据,但由于该数据在某一时刻过期失效,导致请求直接打到数据库,引发数据库负载激增。
4.2、解决方案
热点数据永不过期
:和缓存雪崩类似,将热点数据设置为永不过期,避免核心数据在短时间内失效。
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(); } }
使用互斥锁
:在缓存失效时,使用互斥锁来防止多个线程同时请求数据库,只有一个线程可以去数据库查询数据,其他线程等待直至数据重新缓存。
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(); } }
异步更新缓存
:在缓存失效之前,先异步更新缓存中的数据,保证数据在过期之前已经得到更新。
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在提升应用性能方面的优势。