什么是缓存击穿
缓存击穿是查询数据库中不存在的数据,如果有用户恶意模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了DB上,对DB产生压力,导致数据库异常。
解决方案
布隆过滤器
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。
maven工程引入guava包:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>
redis实现代码
public String getByKey(String key) {
// 通过key获取value
String value = redis.get(key);
if (StringUtil.isEmpty(value)) {
if (bloomFilter.mightContain(key)) {
value = xxxService.get(key);
redis.set(key, value);
return value;
} else {
return null;
}
}
return value;
}
布隆过滤器判断一个数是否存在于一个百万级别的集合中,只要0.2ms就可以完成,性能极佳。BloomFilter的默认的容错率是0.03。
要注意的是:误判率越低,底层维护的数组越长,占用空间越大。因此,误判率实际取值,根据服务器所能够承受的负载来决定;布隆过滤器不支持删除操作。
使用互斥锁
该方法是比较普遍的做法,在根据key获得的value值为空时加锁,再从数据库加载,加载完成后释放锁。若其他线程获取锁失败时睡眠一段时间后重试。
单机环境用并发包的普通锁(synchronized、Lock)类型就行,集群环境则使用分布式锁(Redis的setnx)
集群环境Redis分布式锁实现如下
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
//其他线程休息50毫秒后重试
sleep(50);
get(key);
}
} else {
return value;
}
}
将空值放入缓存
如果查询数据库也为空,也设置一个默认空值存放到缓存,并设置一定的失效时间(不超过五分钟),这样第二次到缓存中获取就有返回值,而不会继续访问数据库,这种办法最简单粗暴。
缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
什么是缓存失效
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,缓存在同一时间内大量键过期,此时有多个进程就请求会全部转发到DB,导致数据库中导致连接异常。
解决方案
使用互斥锁
与解决缓存击穿问题一样用加锁排队,实现同上。
在失效上加随机值
还有一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,避免引发集体失效。