redis 缓存穿透、击穿、雪崩
缓存穿透
缓存穿透指的是,当我们访问某个缓存 key 时,若此 key 不存在于缓存中,则会去查DB数据库,但也没有数据,而后续请求都直接打到DB数据库上。在流量大时,可能DB数据库就挂掉了。缓存和数据库都查询不到这条数据
的现象我们称为缓存穿透
。要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决办法
- 给缓存手动设置一个默认值,不去查询DB数据库。
- 把查询DB数据的结果都放入缓存。即使查询返回的为空,仍然将这个空结果存入到缓存中,但是它的过期时间非常的短,如30秒,最长不会超过5分钟。这样可以防止攻击者反复暴力攻击。
- 使用布隆过滤器(Bloom Filter),这个也能很好地防止缓存穿透。原理就是利用高效的数据结构和算法快速判断出你这个 Key 是否在 DB 中存在,不存在你 return 就好了,存在你就去查了 DB 刷新 KV 再 return。
这样处理的原因是,即使当前查询的 key 为空字符串,或者空数组,非null值,结果也会被缓存起来。后续请求进来发现已经有缓存了,就直接走缓存。
缓存击穿
在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好缓存时间到期,失效了,就会导致大量的请求都打到DB数据库上面去。这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。缓存中没有数据值,但数据库中有此值(一般是缓存时间到期),这种现象我们称为缓存击穿。
解决办法
- 设置热点数据永远不过期。
- 使用互斥锁。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去查询数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行查询数据库的操作并回设缓存;否则,就重试整个 get 缓存的方法。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
若系统的并发很高,当缓存过期时,则大量的请求会穿透缓存,同时到DB中查询,那我们可以设置缓存当缓存过期时,只去DB中请求一次并缓存吗?可以,我们可以使用 redis 的setnx(),意思是(set Not Exists),如果$key不存在则设置,存在则不进行任何操作。设置成功设置返回1,说明当前的请求获得了当前的操作权限,设置失败返回0,说明此资源已经被其他请求获得
给存入缓存的数据增加一个过期时间字段暂时:
public function getArticlesLock($key)
{
$time = time();
$expire = 10 * 2;
$lockKey = 'lock:k';
$data = Cache::get($key);
if (!is_null($data)) {
//缓存未过期
if ($data['expire'] > time()){
return $data['data'];
}
//加锁失败说明已经有请求执行加锁,返回之前的缓存数据
if (!Redis::setnx($lockKey,1)) {
return $data['data'];
}
}
sleep(3);
$datat = $this->searchDB();
$data = [
'data' => $datat,
'expire' => $time + $expire - 10
];
$r = Cache::put($key, $data, $expire);
//解锁
Redis::del($lockKey);
return $data['data'];
}
缓存雪崩
某时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB数据库上面,DB数据库压力瞬间增大,结果就是DB数据库支撑不住而崩溃。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决办法
- 设置热点数据永远不过期。
- 设置不同的失效时间。为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。比如在一个基础的时间上加上或者减去一个范围内的随机值。
- 缓存的高可用性。缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
- 缓存备份和快速预热。比如:Redis持久化机制,尽快恢复缓存集群。一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
目的就是在系统上线前,将数据加载到缓存中。