定义:
缓存击穿就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
1. 互斥锁:(mutex key)
业界比较常用的做法是使用Redis的互斥锁(mutex key)。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去查询数据库加载数据, 而是先去set一个mutex key,当操作返回成功时(意味着获得了互斥锁),再进行查库操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX 是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
public Object getIPhone() {
int cacheTime = 300;
String cacheKey = "iPhone";
String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//查询数据库,先获取锁,需要分情况考虑
String lockKey = "lock";
//需要设置过期时间,防止此线程挂掉之后,其他线程也无法加锁
Boolean lockResult = CacheHelper.setnx(lockKey,"mylocker",30)
if (lockResult) {//情况1:加锁成功
cacheValue = DBHelper.getIPhoneFromDB();
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = "";
}
CacheHelper.add(cacheKey, cacheValue, cacheTime);
CacheHelper.delete(lockKey);
return cacheValue;
} else {//情况2:加锁失败
Thread.sleep(30);
cacheValue = CacheHelper.get(cacheKey);
return cacheValue;
}
}
}
上述代码看似完美,但是存在问题:虽然加锁成功了,但如果查库的时间过长,导致锁失效了,最后delete锁的时候,删掉的是其他线程加的锁。这个时候应该是“各加各锁,各删各锁”,如下所示:
//查询数据库,先获取锁,需要分情况考虑
String lockKey = "lock";
String lockValue = UUID.randomUUID().toString();
//需要设置过期时间,防止此线程挂掉之后,其他线程也无法加锁
Boolean lockResult = CacheHelper.setnx(lockKey,lockValue,30)
if (lockResult){//情况1:加锁成功
cacheValue = DBHelper.getIPhoneFromDB();
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = "";
}
CacheHelper.add(cacheKey, cacheValue, cacheTime);
if (lockValue.equals(CacheHelper.get(lockKey))) {
CacheHelper.delete(lockKey);
}
return cacheValue;
}
缺点:最大问题是,线程相互等待,性能较差
2. 逻辑过期【不设置TTL】:
也就是说不设置Redis的过期时间,但是在数据内添加一个过期的日期,如果用户获取到该数据时,在进行判断是否是过期数据,是则进行重新构建缓存信息。