缓存穿透
查询一个不存在的数据,mysql查不到数据也不会写入缓存,每次请求都会请求到数据库,如果请求太多了会压倒数据库。
解决方案:布隆过滤器;
在添加缓存的时候,也同时把数据写到一下布隆过滤器上。这样每次请求过来的时候就会先请求到布隆过滤器上,布隆过滤器会检索一个元素是否在一个集合中。看这个请求数据经过三次哈希计算得到的槽点,有没有打到布隆过滤器是bitmap数组值为1的节点。
缓存击穿
大量请求同时针对一个缓存资源的请求,这个时候缓存资源又刚刚过期,这个时候请求会直接达到数据库上面,导致宕机。
解决方案:
强一致方案:加互斥锁:线程1没有命中缓存以后,会获取一个redisson的非公平锁,然后查询数据库并且完成缓存重建,然后释放锁;如果在线程1还没有释放锁的时候,线程2来了,他会获取锁失败,然后它会不断地尝试获取锁,直到线程1释放锁,才能拿到缓存数据。
高可用方案:添加逻辑过期时间;在我们添加缓存数据的时候,再添加一个过期时间字段。
当线程1来的时候,如果发现逻辑时间已经过期了,他会获得redisson非公平锁,然后开启一个新线程去查询数据库和完成缓存重建,并且重置逻辑过期时间。这个时候线程1会返回过期数据,然后释放锁。如果有个线程3在线程1还没有释放锁的时候来了,它会获取锁失败,然后放回过期数据。如果有个线程4,它是在完成缓存重建以后来的,那么他会获取到没有过期的缓存数据。
注意:一开始这个key过期是因为系统中给这个缓存设置了过期时间,而我们添加的逻辑过期时间是数据的一部分,线程请求资源的时候会先查看这个时间戳有没有过期,过期了就返回过期数据,并且开启一个新线程去查询数据库完成缓存重建。
import redis.clients.jedis.Jedis;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CacheExample {
private Jedis jedis;
private Lock lock = new ReentrantLock();
public CacheExample(Jedis jedis) {
this.jedis = jedis;
}
public String get(String key) {
String value = jedis.get(key);
if (value == null) {
return "Not found";
}
// 解析缓存中的值和逻辑过期时间
CacheValue cacheValue = parseCacheValue(value);
long currentTime = System.currentTimeMillis();
// 如果逻辑过期时间已到
if (currentTime > cacheValue.getExpirationTime()) {
// 返回过期数据
new Thread(() -> {
try {
// 尝试获取锁
if (lock.tryLock()) {
try {
// 再次检查缓存,确保没有其他线程已经更新
value = jedis.get(key);
if (value != null) {
cacheValue = parseCacheValue(value);
if (currentTime <= cacheValue.getExpirationTime()) {
return; // 已经有其他线程更新了缓存
}
}
// 加载数据并更新缓存
String dataFromDB = loadDataFromDatabase(key);
long newExpirationTime = currentTime + 60 * 1000; // 设置新的过期时间为 60 秒后
jedis.set(key, new CacheValue(dataFromDB, newExpirationTime).toString());
} finally {
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 返回过期数据
return cacheValue.getValue();
} else {
// 过期时间未到,直接返回缓存中的值
return cacheValue.getValue();
}
}
private String loadDataFromDatabase(String key) {
// 从数据库加载数据
return "Data from DB for " + key;
}
private CacheValue parseCacheValue(String value) {
// 解析缓存中的值和过期时间
String[] parts = value.split(",");
return new CacheValue(parts[0], Long.parseLong(parts[1]));
}
private static class CacheValue {
private String value;
private long expirationTime;
public CacheValue(String value, long expirationTime) {
this.value = value;
this.expirationTime = expirationTime;
}
public String getValue() {
return value;
}
public long getExpirationTime() {
return expirationTime;
}
@Override
public String toString() {
return value + "," + expirationTime;
}
}
}
缓存雪崩
是指同一时间大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力,压倒数据库。
解决方案:
第一种情况:给不同的Key的TTL添加随机值
第二种情况:利用Redis集群提高服务的可用性