缓存穿透和缓存雪崩,相信使用过缓存的同学多少都有接触过。缓存穿透,顾名思义就是请求直接穿过了缓存访问了数据库,缓存形同虚设不起作用。缓存雪崩则是某一时刻因缓存失效导致大量请求打到数据库,影响服务稳定性,甚至影响到下游的服务。
出现场景
缓存穿透出现的情况一般都是数据无中生有,正常处理缓存的逻辑是:服务器接收到用户请求,先查缓存中是否存在数据,存在直接返回,不存在则到数据库中查询数据添加数据到缓存中,同时将数据返回。反映到代码里则是:
public Product getDetailInfo(int id) {
String keys = CACHE_KEY + id;
Product info = (Product)redisTemplate.opsForValue().get(id);
if (info != null){
//滑动过期
redisTemplate.expire(keys, 20000, TimeUnit.MILLISECONDS);
return info;
}
/**
* TODO 数据查询
*/
if (info != null){
//添加数据到缓存
redisTemplate.opsForValue().set(keys, info);
//设置过期
redisTemplate.expire(keys, 20000, TimeUnit.MILLISECONDS);
}
return info;
}
这里便存在一个问题,如果用户请求的数据是数据中不存在的,那么则会绕过缓存直接查数据库,就像穿过了缓存一样。如果这时候用户一直发送请求,则会给数据库带来很大的压力甚至数据库假死,从而影响服务稳定性。
缓存雪崩则是某一个时刻,刚好有个热点数据缓存失效,刚好这个时刻访问这个热点数据的用户特别多,导致有大量的请求访问了数据库,给数据库造成了巨大的压力。
解决方案
对于缓存穿透的问题,如果是少量不存在的key可以直接缓存一个空值,如果不存在的key比较多,可采用加入一个布隆过滤器进行校验,这里采用布隆过滤器的方式。对于项目启动之前将数据预先加到布隆过滤器里,下次查询的时候则先判断布隆过滤器里数据是否存在,存在则说明数据正常,不存在则直接返回。
@Service("productService")
public class ProductServiceImpl implements ProductService{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostConstruct
public void init(){
ArrayList<Product> infoList = new ArrayList<Product>();
/**
* TODO 查询数据库中所有数据
*/
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), infoList.size());
for (Product info : infoList) {
bloomFilter.put(CACHE_KEY + info.getId());
}
}
@Override
public Product getDetailInfo(int id) throws Exception{
String keys = CACHE_KEY + id;
//判断bloom过滤器中是否存在
if(!bloomFilter.mightContain(keys)){
return null;
}
//查询缓存
Product info = (Product)redisTemplate.opsForValue().get(keys);
if (info != null){
//滑动过期
redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
return info;
}
/**
* TODO 数据查询
*/
if (info != null){
//添加数据到缓存
redisTemplate.opsForValue().set(keys, info);
//设置过期
redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
}
return info;
}
private BloomFilter<String> bloomFilter = null;
private static final String CACHE_KEY = "PRODUCT::";
}
对于缓存雪崩,则可以基于锁实现,可以对请求的key在数据库访问阶段加锁,让同一时刻同一类型的请求只有一个能访问到数据库,后续请求则访问缓存就可获取,避免了大量请求访问数据库,这里采用ConcurrentHashMap<String, Lock>实现不同类型锁的绑定。
@Service("productService")
public class ProductServiceImpl implements ProductService{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostConstruct
public void init(){
//在bean初始化完成后,实例化bloomFilter,并加载数据
ArrayList<Product> infoList = new ArrayList<Product>();
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), infoList.size());// 32个
for (Product info : infoList) {
bloomFilter.put(CACHE_KEY + info.getId());
}
}
@Override
public Product getDetailInfo(int id) {
String keys = CACHE_KEY + id;
//判断bloom过滤器中是否存在
if(!bloomFilter.mightContain(keys)){
return null;
}
//查询缓存
Product info = (Product)redisTemplate.opsForValue().get(keys);
if (info != null){
//滑动过期
redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
return info;
}
doLock(keys);
try {
/**
* TODO 数据查询
* 这里也可以在查询前再查一次缓存,不加会查多几次db,也没太大关系
*/
if (info != null){
//添加数据到缓存
redisTemplate.opsForValue().set(keys, info);
//设置过期
redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
}
}catch (Exception e){
LOGGER.error("info err;exp={};id={}", e.getMessage(), id);
}finally {
releaseLock(keys);
}
return info;
}
private void doLock(String keys) {
ReentrantLock newLock = new ReentrantLock();
//putIfAbsent 若已存在,则newLock会被直接丢弃
Lock oldLock = locks.putIfAbsent(keys, newLock);
if(oldLock == null){
newLock.lock();
}else{
oldLock.lock();
}
}
private void releaseLock(String keys) {
ReentrantLock oldLock = (ReentrantLock)locks.get(keys);
if(oldLock != null && oldLock.isHeldByCurrentThread()){
oldLock.unlock();
}
}
private BloomFilter<String> bloomFilter = null;
private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(ProductService.class);
private static final String CACHE_KEY = "PRODUCT::";
}
代码在原来穿透的基础上添加了锁操作,这里keys不同才会创建锁,实现同一种类型的keys阻塞,不同类型的keys之间互不影响,能够一定程度提高系统的性能。
小结
以上便是”缓存穿透和缓存雪崩的处理方案“的所有内容,内容基于Redis缓存讲解,但思想其实也适合其他缓存应用。如果您有什么疑问或者文章有什么问题,欢迎私信或留言交流~