缓存击穿、缓存穿透和缓存雪崩
缓存击穿
情形描述
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
使用互斥锁(mutex key)
在缓存失效的时间点(拿到的值为空),不立即从数据库中取数据,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX 是 “Set If Not Exists”的缩写,只有不存在的时候才会进行设置,可以利用它来实现锁的效果。(2.6.1之前未实现setnx的过期期间)
2.6.1之前
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}
2.6.1之后
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 { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
“提前”使用互斥锁
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
设置永不过期
在物理层面:将热点key的有效期设置为不过期。在逻辑上呢,将过期时间放在key对应的value中,如果发现即将过期,通过后台的线程进行缓存的重新构建。
这种方法的有点是性能比较好,缺点就是在构建缓存的过程中,其他线程会访问到旧数据。这个大部分情况下是可以忍受的。
资源保护
采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
方法总结
解决方案 | 优点 | 缺点 |
---|---|---|
简单分布式互斥锁(mutex key) | 1. 思路简单 2. 保证一致性 | 1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
“提前”使用互斥锁 | 1. 保证一致性 | 同上 |
不过期 | 1. 异步构建缓存,不会阻塞线程池 | 1. 不保证一致性。 2. 代码复杂度增大(每个value都要维护一个timekey)。 3. 占用一定的内存空间(每个value都要维护一个timekey)。 |
资源隔离组件hystrix | 1. hystrix技术成熟,有效保证后端。 2. hystrix监控强大。 | 1. 部分访问存在降级策略。 |
缓存雪崩
情形
有大面积的缓存在同一时间失效,请求全部被转发到数据库中,导致这一瞬间数据库的压力陡增,造成雪崩。
解决方案
将缓存失效时间分散
如将缓存的有效期设置为一个2-5分钟的随机数。这样就会让缓存的失效时间错开,不会存在同一时间里缓存大面积失效的问题。
加锁或队列
将用户的请求进行加锁排队来分散数据库的压力,但是本质上并没有提高系统的吞吐量,而且可能造成阻塞,导致用户等待超时。而如果是在分布式环境下,还要解决分布式锁的问题,所以在高并发场景下很少使用。
二级缓存或者双缓存
A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
缓存穿透
情形
大量的请求都查询一个不存在的数据,如id=-1等等,由于数据库中本就没有数据,自然不存在缓存命中这么一说,如此一来,所有的流量都进入到了数据库中,如果被利用这一点使用一个不存在的key进行频繁的攻击,可能会导致数据库挂掉等风险。
解决方案
将空数据写入缓存
即使查询结果为空,也将其写入缓存中,但是过期时间很短。用这种方法可以避免同一时间数据库的负载过大。
采用布隆过滤器
将所有可能存在的数据放在一个bitmap中,使用这个bitmap对不存在的数据进行拦截。