缓存穿透的解决方案
方案一: 缓存获取失败后直接访问数据库
方案
对于热点数据,如果直接访问数据库获取数据就会造成数据库压力过大,很可能直接把数据库打宕机。所以我们通常会用redis加一层缓存,就有了以下代码
public String getData(Long key) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.KEY_NAME + key;
String data = redisService.getValue(cacheKey);
if (null != data) return data;
String dbData = yourDao.query(key);
if (null != dbData) {
data = dbData;
}
redisService.setValue(cacheKey, data);
return data;
}
先尝试从redis获取数据,如果不存在则从数据中获取,然后将数据写入redis,最后将数据返回。
潜在问题
在海量并发的情况下,大量的请求会在拿不到redis中的数据后,访问数据库。哪怕第一个请求将数据写入缓存,别的请求也走到访问数据库的流程了,这就是缓存击穿
方案二:使用分布式锁
方案
通过使用分布式锁,获取到锁的请求才能去访问数据库
public String getData(Long key) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.KEY_NAME + key;
String data = redisService.getValue(cacheKey);
if (null != data) return data;
RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
lock.lock();
try {
String dbData = yourDao.query(key);
if (null != dbData) {
data = dbData;
}
redisService.setValue(cacheKey, data);
} finally {
lock.unlock();
}
return data;
}
潜在问题
虽然加了锁,但是每一个请求还是会在等到锁之后去访问数据库。这样造成了严重的性能问题:多余的查表操作,和请求之间的锁竞争。第一个请求将数据写入缓存之后,后续请求应该从缓存中查询,没必要查表
方案三:双重检查锁
方案
使用单例模式双重检查锁的思想,两次判断确定数据确实为空后再去查表
public String getData(Long key) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.KEY_NAME + key;
String data = redisService.getValue(cacheKey);
if (null != data) return data;
RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
lock.lock();
try {
// 双重检查
data = redisService.getValue(cacheKey);
if (null != data) return data;
String dbData = yourDao.query(key);
if (null != dbData) {
data = dbData;
}
redisService.setValue(cacheKey, data);
return data;
} finally {
lock.unlock();
}
}
第一个请求将数据写入缓存后,之后的请求可以直接从缓存中读取数据
潜在问题
虽然避免了多余的查表操作,但是任然后很多请求会遇到锁竞争的问题,只有等之前的请求将锁释放之后才能执行后续流程。最后一个请求的接口响应时间会非常长
方案四:tryLock减少锁竞争
方案
设置锁的等待时间,超过等待时间后,继续执行后续流程
public String getData(Long key) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.KEY_NAME + key;
String data = redisService.getValue(cacheKey);
if (null != data) return data;
RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
boolean lockResult = false;
try {
// 等待1秒,超过等待时间后继续执行
lockResult = lock.tryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// 处理异常 线程中断异常
}
try {
// 双重检查
data = redisService.getValue(cacheKey);
if (null != data) return data;
String dbData = yourDao.query(key);
if (null != dbData) {
data = dbData;
}
redisService.setValue(cacheKey, data);
return data;
} finally {
if (lockResult) {
lock.unlock();
}
}
}
第一个请求获得了锁,将数据从数据库写入缓存。其余的请求等待1秒,如果超过1秒就不再竞争锁,继续执行。因为第一个请求已经写数据到缓存,所以可以直接读缓存返回。
潜在问题
如果第一个请求执行的时间超过了别的请求的等待时间,没来得及写入数据到redis,别的请求就会继续访问数据库。所以需要根据业务来决定合适的等待锁时间。这个方案牺牲了绝对安全来优化锁竞争的性能问题。设计方案时需要根据业务进行评估,决定应该使用哪一个方案。

浙公网安备 33010602011771号