缓存击穿的概念
缓存击穿(Cache Breakdown)指的是在某一个热点缓存数据过期的瞬间,有大量并发请求同时访问这个数据,而该数据在缓存中不存在,因此所有的请求都打到数据库上,导致数据库压力过大,可能引起系统性能问题。
解决缓存击穿的方法
为了解决缓存击穿问题,可以采取以下策略:
- 互斥锁(Mutex):在缓存失效时,只有一个线程去加载数据,其他线程等待。
- 永不过期:热点数据的缓存永不过期,只在数据更新时主动去更新缓存。
- 预加载:在缓存即将过期之前,提前加载数据到缓存。
以下是这几种解决方法的详细代码示例:
1. 互斥锁(Mutex)
通过加锁的方式,控制缓存失效时只有一个线程去加载数据,其他线程等待,从而避免大量请求同时打到数据库。
示例代码:
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
public class CacheBreakdownWithLockExample {
private Jedis jedis;
public CacheBreakdownWithLockExample(Jedis jedis) {
this.jedis = jedis;
}
public String getCachedData(String key, DataProvider provider, int cacheTime) {
String value = jedis.get(key);
if (value != null) {
return value;
}
String lockKey = key + ":lock";
String requestId = String.valueOf(Thread.currentThread().getId());
// 尝试加锁
boolean locked = tryGetLock(lockKey, requestId, 30000); // 锁定30秒
if (locked) {
try {
value = provider.getData();
if (value != null) {
jedis.setex(key, cacheTime, value);
}
} finally {
releaseLock(lockKey, requestId);
}
} else {
// 等待一段时间后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCachedData(key, provider, cacheTime);
}
return value;
}
private boolean tryGetLock(String lockKey, String requestId, int expireTime) {
SetParams params = new SetParams().nx().px(expireTime);
String result = jedis.set(lockKey, requestId, params);
return "OK".equals(result);
}
private void releaseLock(String lockKey, String requestId) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
public interface DataProvider {
String getData();
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
CacheBreakdownWithLockExample cache = new CacheBreakdownWithLockExample(jedis);
String key = "dataKey";
int cacheTime = 3600; // 缓存 1 小时
String value = cache.getCachedData(key, () -> {
// 模拟数据库查询
return "dataValue";
}, cacheTime);
System.out.println("Cached Value: " + value);
jedis.close();
}
}
2. 永不过期
对于热点数据,设置其缓存为永不过期,只在数据更新时主动去更新缓存。
示例代码:
java
import redis.clients.jedis.Jedis;
public class CacheBreakdownWithNoExpireExample {
private Jedis jedis;
public CacheBreakdownWithNoExpireExample(Jedis jedis) {
this.jedis = jedis;
}
public void setCachedData(String key, String value) {
jedis.set(key, value); // 设置为永不过期
}
public String getCachedData(String key) {
return jedis.get(key);
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
CacheBreakdownWithNoExpireExample cache = new CacheBreakdownWithNoExpireExample(jedis);
String key = "dataKey";
String value = "dataValue";
// 设置缓存数据
cache.setCachedData(key, value);
// 获取缓存数据
String cachedValue = cache.getCachedData(key);
System.out.println("Cached Value: " + cachedValue);
jedis.close();
}
}
3. 预加载
在缓存即将过期之前,提前加载数据到缓存中,防止缓存失效带来的问题。
示例代码:
java
import redis.clients.jedis.Jedis;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CacheBreakdownWithPreloadExample {
private Jedis jedis;
public CacheBreakdownWithPreloadExample(Jedis jedis) {
this.jedis = jedis;
}
public void setCachedData(String key, String value, int cacheTime) {
jedis.setex(key, cacheTime, value);
}
public String getCachedData(String key) {
return jedis.get(key);
}
public void preloadCache(String key, DataProvider provider, int cacheTime, int preloadTime) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
String value = provider.getData();
if (value != null) {
jedis.setex(key, cacheTime, value);
}
}, 0, preloadTime, TimeUnit.SECONDS);
}
public interface DataProvider {
String getData();
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
CacheBreakdownWithPreloadExample cache = new CacheBreakdownWithPreloadExample(jedis);
String key = "dataKey";
int cacheTime = 3600; // 缓存 1 小时
int preloadTime = 3500; // 预加载时间 3500 秒
// 模拟数据预加载
cache.preloadCache(key, () -> {
// 模拟数据库查询
return "dataValue";
}, cacheTime, preloadTime);
// 获取缓存数据
String cachedValue = cache.getCachedData(key);
System.out.println("Cached Value: " + cachedValue);
jedis.close();
}
}
总结
通过以上示例代码,您可以分别采用互斥锁、永不过期和预加载等方法来解决Redis的缓存击穿问题。合理使用这些方法,可以有效避免热点数据失效时对数据库的瞬时压力,提高系统的稳健性和可用性。