面试题:什么是Reids的击穿、穿透、雪崩三种现象?如何解决?

目录

击穿

产生原因

解决思路

穿透

产生原因

解决思路

雪崩

产生原因

解决思路


扫描二维码关注公众号,回复: 14771713 查看本文章

击穿

缓存击穿就是在处于集中式高并发访问的情况下,当某个热点 key 在失效的瞬间,大量的请求在缓存中获取不到。瞬间击穿了缓存,所有请求直接打到数据库,就像是在一道屏障上击穿了一个洞。

产生原因

  • Key 过期

在 Redis 中,key 有过期时间。如果某一时刻(淘宝秒杀,双十一零点开始)key 失效,那么零点之后对某个 key 失效的商品的所有请求将会直接打到数据库上,很有可能倒是数据库崩掉,仅而造成整个服务的不可用。

  • Key 被内存淘汰机制淘汰

因为内存是有限的,时时刻刻都有新的缓存数据被放到内存中,而旧的数据被淘汰移除内存。如果开启了 Redis 的内存淘汰机制,Key 会存在被所设置的内存淘汰机制所淘汰的情况。

解决思路

正常的处理请求如下图:

 由于 key 过期在所难免(注意:非必要情况下不必不要设置缓存时间永不过期),根据 Redis 的单线程特性,可认为任务是在队列中依次执行的。当请求到达 Redis 发现 key 已经过期时,进行一个操作:设置锁

具体流程大概如下:

①限流

②设置锁
1.获取 Redis 锁,如果没有获取到,则回到任务队列继续排队
2.获取到锁,从数据库拉取数据并放入缓存中
3.释放锁,其他请求从缓存中拿到数据

但是引出了一个新的问题,如果获取到锁去数据库拿数据的请求挂了怎么办?这就造成锁没有释放,而其他进程都在等待锁释放。解决办法是:对锁设置一个过期时间,如果达到了锁的过期时间还没释放就自动释放

这样又出现了一个新的问题,如果是锁超时呢?也就是在设定的时间里数据没有取出来,但是锁确过期了。常见的思路是:锁过期时间值递增。但是这样做不是很合理,因为第一个请求可能超时,如果后面的也超时呢?接连多次超时之后,过期时间值势必非常大,这样做弊端太多。另外一个思路是:再开启一个线程,进行监控。如果取数据的线程没有挂的话,就适当延迟锁的过期时间。

注意:

通过 setNX 获取锁,如果成功了则更新缓存然后删除锁。其实这里有一个严重的问题:如果更新缓存的时候因为某些原因意外退出了,那么这个锁就不会被删除而一直存在,以至于缓存再也得不到更新。为了解决这个问题有人可能会想到给锁设置一个过期时间,如下:

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

因为 setNX 不具备设置过期时间的功能,所以要借助 Expire 来设置,同时需要使用 Multi()/Exec() 来确保请求的原子性,以免 setNX 成功了 Expire 却失败了。这样还有问题:当多个请求到达时,虽然只有一个请求的 setNX 可以成功,但是任何一个请求的 Expire 却都可以成功。这就意味着即便获取不到锁也可以刷新过期时间,导致锁一直有效。还是解决不了上面的问题,显然 setNX 满足不了需求。

Redis从 2.6.12 起,SET 涵盖了 SETEX 的功能, SET命令的行为可以通过一系列参数来修改。SET 本身又包含了设置过期时间的功能,所以使用 SET 就可以解决上面遇到的问题。

$rs = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($rs) {
    // 处理更新缓存逻辑
    // ......
    // 删除锁
    $redis->del($key);
}

Redis SET 命令

SET key value [EX seconds] [PX milliseconds] [NX|XX]

EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。

  • PXmilliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SETkeyvaluePXmilliseconds 的效果等同于执行 PSETEXkeymillisecondsvalue 。
  • NX : 只在键不存在时, 才对键进行设置操作。 执行 SETkeyvalueNX 的效果等同于执行 SETNXkeyvalue 。
  • XX : 只在键已经存在时, 才对键进行设置操作。

穿透

产生原因

穿透主要原因是很多请求都在访问数据库一定不存在的数据,造成请求将缓存和数据库都穿透的情况。

例如一个卖书的商城一直被请求查询茶叶产品,由于 Redis 缓存主要是用来缓存热点数据,此数据是在数据库上存在的。对于数据库都不存在的数据,是没法缓存的。这种异常流量就会直接到达数据库并且返回“NULL”的查询结果,例如查找id为-1的数据访问量有1000次,但是数据库和缓存中一般都不会存在id为负数的情况所以。访问量又会直接打击到数据库导致数据库崩盘。

(一些恶意攻击就会这样干)

解决思路

应对这种请求,处理办法有:

  • 规则排除

可以增加一些参数检验。例如数据库数据 id 一般都是递增的,如果请求 id = -10 这种参数,势必绕过Redis。避免这种情况,可以对用户真实性检验等操作。

  • null值填充

当缓存穿透时,redis存入一个类似null的值,下次访问则直接缓存返回空,当数据库中存在该数据的值则需要把redis存在的null值清除并载入新值,此方案不能解决频繁随机不规则的key请求。

  • 一级二级缓存法/布隆过滤器

例如布隆过滤器、增强版布隆过滤器、布谷鸟过滤器(使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率;)。如下图:


雪崩

雪崩和击穿类似,不同的是击穿是一个热点 Key 某时刻失效,而雪崩是大量的热点 Key 在一瞬间失效。当大量缓存的过期时间相同时,缓存到达过期时间集体失效或者未加载到内存中,大量请求绕过缓存层直接访问数据库 load 数据,导致数据库频繁 IO,性能下降乃至宕机崩溃。

例如:双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存30分钟。那么到了凌晨零点半的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

解决思路

  • 随机过期时间策略,尽量让缓存失效的时间均匀分布。

可以采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

注意:但是随机过期时间策略在有时点性的情况下是有问题的方案。举个例子:银行做活动,之前这个利息系数为2%,过了零点系数改为3%,这种情况能将用户的对应的 key 改为随机过期吗?明显不可以,同样存钱,你存到年底利息300万,隔壁老王才200万,银行经理不被天天打才怪。

  • 触发批量更新

正确的思路是:首先要看看这个 Key 过期是不是时点性有关,时点性无关的话,可以随机过期时间解决。如果是时点性有关,例如上述的银行利率调整,那么就要利用强依赖击穿方案,策略是先过去的线程更新一下所有 key。这样带来的问题是一次对要对批量更新操作加上锁,防止重复更新,也要保证批量更新的时间不会对用户体验带来影响。

  • 加锁排队

并发量不是特别多的时候,使用最多的解决方案是加锁排队。加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间 key 是锁着的,这是过来 1000 个请求 999 个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题。线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用。

  • 缓存标记

给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

缓存数据:它的过期时间比缓存标记的时间延长 1 倍。例:标记缓存时间 30 分钟,数据缓存设置为 60 分钟。 这样,当缓存标记 key 过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

  • 设置redis集群和DB集群的高可用

如果redis出现宕机情况,可以立即由别的机器顶替上来。这样可以防止一部分的风险。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。但是缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

每个解决方案都是有一定的适合场景,也可能需要自己想办法来设计一种适合具体场景的解决方案。在于遇到生产环境相应的问题时,不可以随便 copy 代码,“三思而后行”。

猜你喜欢

转载自blog.csdn.net/m0_67094505/article/details/127680515