缓存击穿的概念
缓存击穿(Cache Breakdown)指的是在某一个热点缓存数据过期的瞬间,有大量并发请求同时访问这个数据,而该数据在缓存中不存在,因此所有的请求都打到数据库上,导致数据库压力过大,可能引起系统性能问题。
解决缓存击穿的方法
为了解决缓存击穿问题,可以采取以下策略:
- 互斥锁(Mutex):在缓存失效时,只有一个线程去加载数据,其他线程等待。
- 永不过期:热点数据的缓存永不过期,只在数据更新时主动去更新缓存。
- 预加载:在缓存即将过期之前,提前加载数据到缓存。
以下是这几种解决方法的详细代码示例:
1. 互斥锁(Mutex)
通过加锁的方式,控制缓存失效时只有一个线程去加载数据,其他线程等待,从而避免大量请求同时打到数据库。
示例代码:
1import redis.clients.jedis.Jedis; 2import redis.clients.jedis.params.SetParams; 3 4public class CacheBreakdownWithLockExample { 5 private Jedis jedis; 6 7 public CacheBreakdownWithLockExample(Jedis jedis) { 8 this.jedis = jedis; 9 } 10 11 public String getCachedData(String key, DataProvider provider, int cacheTime) { 12 String value = jedis.get(key); 13 if (value != null) { 14 return value; 15 } 16 17 String lockKey = key + ":lock"; 18 String requestId = String.valueOf(Thread.currentThread().getId()); 19 20 // 尝试加锁 21 boolean locked = tryGetLock(lockKey, requestId, 30000); // 锁定30秒 22 if (locked) { 23 try { 24 value = provider.getData(); 25 if (value != null) { 26 jedis.setex(key, cacheTime, value); 27 } 28 } finally { 29 releaseLock(lockKey, requestId); 30 } 31 } else { 32 // 等待一段时间后重试 33 try { 34 Thread.sleep(100); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 return getCachedData(key, provider, cacheTime); 39 } 40 return value; 41 } 42 43 private boolean tryGetLock(String lockKey, String requestId, int expireTime) { 44 SetParams params = new SetParams().nx().px(expireTime); 45 String result = jedis.set(lockKey, requestId, params); 46 return "OK".equals(result); 47 } 48 49 private void releaseLock(String lockKey, String requestId) { 50 if (requestId.equals(jedis.get(lockKey))) { 51 jedis.del(lockKey); 52 } 53 } 54 55 public interface DataProvider { 56 String getData(); 57 } 58 59 public static void main(String[] args) { 60 Jedis jedis = new Jedis("localhost", 6379); 61 CacheBreakdownWithLockExample cache = new CacheBreakdownWithLockExample(jedis); 62 63 String key = "dataKey"; 64 int cacheTime = 3600; // 缓存 1 小时 65 66 String value = cache.getCachedData(key, () -> { 67 // 模拟数据库查询 68 return "dataValue"; 69 }, cacheTime); 70 71 System.out.println("Cached Value: " + value); 72 73 jedis.close(); 74 } 75} 76
2. 永不过期
对于热点数据,设置其缓存为永不过期,只在数据更新时主动去更新缓存。
示例代码:
1import redis.clients.jedis.Jedis; 2 3public class CacheBreakdownWithNoExpireExample { 4 private Jedis jedis; 5 6 public CacheBreakdownWithNoExpireExample(Jedis jedis) { 7 this.jedis = jedis; 8 } 9 10 public void setCachedData(String key, String value) { 11 jedis.set(key, value); // 设置为永不过期 12 } 13 14 public String getCachedData(String key) { 15 return jedis.get(key); 16 } 17 18 public static void main(String[] args) { 19 Jedis jedis = new Jedis("localhost", 6379); 20 CacheBreakdownWithNoExpireExample cache = new CacheBreakdownWithNoExpireExample(jedis); 21 22 String key = "dataKey"; 23 String value = "dataValue"; 24 25 // 设置缓存数据 26 cache.setCachedData(key, value); 27 28 // 获取缓存数据 29 String cachedValue = cache.getCachedData(key); 30 System.out.println("Cached Value: " + cachedValue); 31 32 jedis.close(); 33 } 34} 35
3. 预加载
在缓存即将过期之前,提前加载数据到缓存中,防止缓存失效带来的问题。
示例代码:
1import redis.clients.jedis.Jedis; 2 3import java.util.concurrent.Executors; 4import java.util.concurrent.ScheduledExecutorService; 5import java.util.concurrent.TimeUnit; 6 7public class CacheBreakdownWithPreloadExample { 8 private Jedis jedis; 9 10 public CacheBreakdownWithPreloadExample(Jedis jedis) { 11 this.jedis = jedis; 12 } 13 14 public void setCachedData(String key, String value, int cacheTime) { 15 jedis.setex(key, cacheTime, value); 16 } 17 18 public String getCachedData(String key) { 19 return jedis.get(key); 20 } 21 22 public void preloadCache(String key, DataProvider provider, int cacheTime, int preloadTime) { 23 ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); 24 scheduler.scheduleAtFixedRate(() -> { 25 String value = provider.getData(); 26 if (value != null) { 27 jedis.setex(key, cacheTime, value); 28 } 29 }, 0, preloadTime, TimeUnit.SECONDS); 30 } 31 32 public interface DataProvider { 33 String getData(); 34 } 35 36 public static void main(String[] args) { 37 Jedis jedis = new Jedis("localhost", 6379); 38 CacheBreakdownWithPreloadExample cache = new CacheBreakdownWithPreloadExample(jedis); 39 40 String key = "dataKey"; 41 int cacheTime = 3600; // 缓存 1 小时 42 int preloadTime = 3500; // 预加载时间 3500 秒 43 44 // 模拟数据预加载 45 cache.preloadCache(key, () -> { 46 // 模拟数据库查询 47 return "dataValue"; 48 }, cacheTime, preloadTime); 49 50 // 获取缓存数据 51 String cachedValue = cache.getCachedData(key); 52 System.out.println("Cached Value: " + cachedValue); 53 54 jedis.close(); 55 } 56} 57
总结
通过以上示例代码,您可以分别采用互斥锁、永不过期和预加载等方法来解决Redis的缓存击穿问题。合理使用这些方法,可以有效避免热点数据失效时对数据库的瞬时压力,提高系统的稳健性和可用性。
《Redis(83)Redis的缓存击穿是什么?》 是转载文章,点击查看原文。
