在高qps的系统中,为了避免每次请求都查询数据库,给数据库造成很大的压力,一般都会使用缓存来减轻数据库的访问压力。不过缓存的一些问题会导致缓存失去应有的作用,使得请求还是访问了数据库,给数据库造成了很大的压力。这些问题包括
- 缓冲穿透
- 缓冲击穿
- 缓冲雪崩
一、缓存穿透
缓冲穿透是指请求查询的数据,在数据库中根本不存在,所以缓存中也不会有,这样每次请求都会查询数据库的现象。
常见的解决方案有两种,一是缓存空值,二是使用布隆过滤器。
1.1 缓存空值
发生缓存穿透,是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。那么我们就可以为这些key在缓存中设置空值null。后面再出现查询这个key 的请求的时候,直接返回设置的空值即可。当然为了健壮性,我们要对这些key设置过期时间,以防止真的出现数据。
1.2 bloom filter(布隆过滤器)
BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。我们把有数据的key都放到BloomFilter中,每次查询的时候都先去BloomFilter判断,如果没有就直接返回null,这样也就不会查询数据库了。
在实际应用中,这两种方法会组合使用,即先经过布隆过滤器,过滤掉不存在的key,然后查询缓存,如果没有数据,那么返回缓存中的空值。另外,BloomFilter没有删除操作,对于删除的key,查询就会经过BloomFilter然后查询缓存再查询数据库,所以对于删除的key,也可以在缓存中存储空值来避免查询数据库。
另外,还可以在接口层做一些基础校验,避免恶意攻击,比如用户鉴权,id<=0的直接拦截等。
二、缓存击穿
缓存击穿是指大量请求同时查询一个key时,这个key刚好失效,使得大量请求都去查询数据库的现象。这样的key一般是热点数据。缓存击穿是单个key的过期问题引起的,而缓存雪崩是大量key过期引起的。
解决方案有两种,一是使用互斥锁或分布式锁,二是设置热点数据永不过期
2.1 互斥锁或分布式锁
当这些请求查询数据库时,只有第一个请求会拿到锁,其他请求的线程因为没有锁而被阻塞,就不会查询数据库。等第一个请求查询后,数据进行缓存,其他请求发现缓存中已经有数据,就不会再查询数据库。
//互斥锁
public static String getDate(String key) throws InterruptedException{
String result = getDataFromCache(key);
if(result == null){
if(reenLock.tryLock()){
result = getDataFromDb(key);
if(result != null){
setDataToCache(key, result);
}
reenLock.unlock();
}else{
Thread.sleep(100);
result = getData(key);
}
}
return result;
}
三、缓存雪崩
缓存雪崩是指由于某些原因,在同一时刻发生大量缓存失效,导致大量请求查询数据库的现象。比如缓存服务器宕机或大量缓存过期时间相同而同时过期等原因。
解决方案
3.1 使用缓存集群
对于缓存服务器宕机的情况,可以使用缓存集群实现高可用性。使用集群的另一个好处是可以把不同热点数据分布在不同缓存服务器上。
3.2 设置分散的过期时间
对于大量缓存同时过期的问题,可以将过期时间分散开,比如在原有过期时间上增加一个随机值,这样缓存的过期时间的重复率就会降低,就很难引发集体失效的情况。
3.3 设置热点数据永不过期