一 缓存击穿
缓存击穿是指某些热点数据在缓存中失效,且在失效瞬间有大量并发请求同时去访问该数据,导致所有请求都落在数据库上,进而对数据库造成极大压力。
例如某一热点数据频繁被访问,缓存过期后,大量请求在缓存没有命中时同时去查询数据库,从而使数据库的负载瞬间升高。
解决方案:
1.使用互斥锁(Mutex)机制:在缓存过期时,只允许一个线程去加载数据,其它线程等待,等数据加载完成后,再从缓存中获取数据。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class CacheWithMutex {
private ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
private ReentrantLock lock = new ReentrantLock();
// 模拟数据库查询
private String loadFromDB(String key) {
// 模拟数据库查询
return "ValueFromDB";
}
public String getValue(String key) {
String value = cache.get(key);
if (value == null) {
// 获取锁
lock.lock();
try {
// 双重检查防止重复查询数据库
value = cache.get(key);
if (value == null) {
// 缓存中没有,查询数据库
value = loadFromDB(key);
cache.put(key, value);
}
} finally {
// 释放锁
lock.unlock();
}
}
return value;
}
}
2.提前设置缓存过期时间:可以给热点数据提前设置不同的过期时间,避免同一时刻大量缓存同时失效。
3.使用永不过期策略:对于特别重要的热点数据,可以考虑将其设置为不过期,但需要定时主动更新缓存。
二 缓存穿透
缓存穿透是指查询的数据既不存在缓存中也不存在数据库中,用户请求每次都会打到数据库。由于缓存机制不生效,导致每个请求都会穿透缓存直接查询数据库,极易对数据库造成压力。
例如用户请求了一个数据库中不存在的数据,且该数据在缓存中也没有命中。由于缓存没有存储该不存在的数据,每次查询都会直接请求数据库。
解决方案:
1.缓存空值:当数据库查询结果为空时,将这个空结果也放入缓存中,并设置较短的过期时间(如5分钟),避免频繁穿透到数据库。
public String getValue(String key) {
String value = cache.get(key);
if (value == null) {
value = loadFromDB(key);
if (value == null) {
// 将空值放入缓存,并设置一个较短的过期时间
cache.put(key, "NULL");
} else {
cache.put(key, value);
}
}
return "NULL".equals(value) ? null : value;
}
2.使用布隆过滤器(Bloom Filter):可以在缓存前增加一个布隆过滤器来判断请求的数据是否存在。布隆过滤器可以高效地判断一个数据是否不存在,大幅减少无效查询。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.util.concurrent.ConcurrentHashMap;
public class CacheWithBloomFilter {
private ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
// 布隆过滤器,容量100000,误判率0.01
private BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 100000, 0.01);
// 模拟数据库查询
private String loadFromDB(String key) {
return null; // 模拟查询不到数据
}
public String getValue(String key) {
// 先检查布隆过滤器是否可能存在
if (!bloomFilter.mightContain(key)) {
return null; // 数据不存在,直接返回
}
// 检查缓存
String value = cache.get(key);
if (value == null) {
// 缓存中没有,查询数据库
value = loadFromDB(key);
if (value == null) {
// 数据库中也没有,放入缓存空值
cache.put(key, "NULL");
} else {
cache.put(key, value);
}
}
return "NULL".equals(value) ? null : value; // 如果缓存的是NULL值,则返回null
}
}
3.参数校验:在请求进入缓存层前,先进行数据校验,比如判断请求是否符合某种格式要求,避免无效的查询。
三 缓存雪崩
缓存雪崩是指缓存中大量数据在同一时间失效,大量请求同时涌入数据库,导致数据库负载过重甚至宕机。这与缓存击穿的区别是,缓存击穿是针对单一热点数据,而缓存雪崩是大量缓存同时失效。
例如大规模缓存设置了相同的过期时间,在缓存失效的瞬间,大量请求直接命中数据库,数据库瞬间压力过大,甚至可能导致服务不可用。
解决方案:
缓存数据的过期时间分散化:避免大量数据的缓存同时失效,可以对不同数据设置不同的过期时间,或者在过期时间的基础上加一个随机值,分散缓存的失效时间。
双重缓存机制:除了设置缓存的过期时间外,还可以引入一个备用缓存层,当第一层缓存失效时,先查询第二层缓存,减少对数据库的压力。
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class CacheWithRandomExpiration {
private ConcurrentHashMap<String, CacheObject> cache = new ConcurrentHashMap<>();
private Random random = new Random();
// 模拟数据库查询
private String loadFromDB(String key) {
return "ValueFromDB";
}
public String getValue(String key) {
CacheObject cacheObject = cache.get(key);
if (cacheObject == null || cacheObject.isExpired()) {
synchronized (this) {
cacheObject = cache.get(key);
if (cacheObject == null || cacheObject.isExpired()) {
String value = loadFromDB(key);
// 设置随机过期时间,比如加上 0-60秒的随机时间
long expirationTime = System.currentTimeMillis() + 1000 * 60 * 5 + random.nextInt(1000 * 60);
cacheObject = new CacheObject(value, expirationTime);
cache.put(key, cacheObject);
}
}
}
return cacheObject.getValue();
}
// 缓存对象类
class CacheObject {
private String value;
private long expirationTime;
public CacheObject(String value, long expirationTime) {
this.value = value;
this.expirationTime = expirationTime;
}
public String getValue() {
return value;
}
public boolean isExpired() {
return System.currentTimeMillis() > expirationTime;
}
}
}
限流和降级:在发生雪崩时,可以使用限流和服务降级机制,减少请求直接落到数据库上,保障数据库的稳定性。