Redis (缓存雪崩、穿透、击穿)

一、缓存雪崩:

概念:

缓存雪崩是指缓存中key大批量到过期时间,而这时大量请求同时打过来,引起数据库压力过大甚至down机

实际生产中举例:

以秒杀活动为例,QPS 达到5000,这时,如果这5000个请求同时访问过来,在redis的缓存没有失效时,这个量级的qps,redis 是可以承受住的。

但是如果这时,所有的key都在某一个时间点失效了,而后台还没来得及重新刷缓存(缓存一般是定时任务主动刷新或修改时才刷新),那么这时,所有的请求就将会都直接打入MySQL 数据库。

可以想象,MySQL 基本是扛不住的,并且重启都没用,因为你重启的速度永远跟不上用户刷新的速度。

这个现象就叫做缓存雪崩。

解决方案:

1、针对key同时失效导致的缓存雪崩

我们平时设置key时,应该避免key的过期时间一样,可以在设置key的有效期时,添加一个随机数,把同时刷新出来的key的有效期设置为不一样的(比如几百毫秒到几十秒)。

当然,也可以采用数据预热的方案,在即将发生大并发访问前,手动触发缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

2、针对redis缓存中间件突然挂掉,导致的缓存雪崩

针对小业务量级,我们可以采用 redis 的 sentinel 哨兵机制;

针对大业务量级,我们可以采用 redis 的 cluster 集群方案去应对。

不论是sentinel、还是cluster,其内部都有完整的自动故障转移,对于master 突然挂掉,都不换影响业务的正常运行。

 

二、缓存穿透

概念:

缓存穿透,是指恶意攻击者频繁查询一个数据库一定不存在的数据。

缓存不命中,就会把请求打到数据库,数据库没有这个数据,也就不会把数据存到缓存里,也就导致同样的请求,始终请求的,实际上都不是缓存,而是直接请求到数据库。

这时,如果有人利用不存在的key频繁请求我们的应用接口,并且量级非常大,就会造成存储层数据库承受不住,可能就会挂掉。

实际生产中举例:

比如有一个接口,www.haveyb.com/getProductInfo?product_id=109&porduct_type=2,

正常使用时,如果请求一个不存在的数据,比如 product_id = 999999999,这个数据实际上在数据库中是不存在的,但是就有那么一个恶意攻击者,拿着这个数据,频繁请求你,当一个一个请求发过来还好。

但如果同时几万个并发打过来呢?你实际上缓存没有命中后,把请求都打到数据库了,但是数据库没有这个数据,也就不能把数据放到缓存里,导致的结果就是请求不断地打到数据库上,数据库实际上是承受不住这么大并发的,就会导致数据库挂掉。

这就是缓冲穿透。

解决方案:

1、不管数据实际上存不存在,我们都把这个键存到缓存中(有效期设置的短一些,比如一分钟到三分钟),然后值设置为一个特定值,业务中如果获取到的结果是这个特定值,则报错返回。

2、另一个方案是使用 redis 的布隆过滤器(Bloom Filter),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

注:PHP 中如何使用布隆过滤器

 

三、缓存击穿

1、概念

缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求到数据库,这时,大并发量可能直接将数据库给挂掉。

注:缓存击穿与缓存雪崩最本质的区别在于

缓存雪崩是由于大量的key同时过期,导致所有请求同时直接打到数据库,造成数据库挂掉;

缓存击穿是由于一个热点key失效,之前一直由缓存挡着的请求,直接发送到数据库,造成数据库挂掉。

生产中举例:

我们有一个获取商家系统后台设置的接口,缓存到redis中,这个接口,其他服务都在时刻调用,比如订单、商品、库存模块。

本来,这个数据在缓存中存在,所有请求都被缓存挡住了,这没什么问题,但是如果这个key突然过期了,那这个热点请求就会被直接打到数据库,在大并发量级的情况下,可能就会把数据库挂掉。

解决方案:

1、设置数据永久不过期(只更新,不删除或过期)

2、对该key 的 value 内部设置一个超时值,当访问时,发现已经达到这个超时值了,就更新缓存,并重置超时值(对于这个key,可以设置为不过期,或者超时值小于外部设置的过期时间)

$key = 'system_setting_106976';
$timestamp = time();
$data = [
    'value' => 12345678,
    'timeout' => $time
];
setnx($key, $data, $time + 360);