go高并发之路——缓存穿透、缓存雪崩
缓存击穿、缓存穿透、缓存雪崩是使用Redis的三个经典问题,上篇文章讲了缓存击穿,今天就讲下剩下的两个问题。
一、缓存穿透
定义:缓存穿透是指查询一个根本不存在的数据,缓存层和DB层都不会命中。这样缓存永远不会生效,这些请求最终都会访问数据库。引起DB的压力瞬间变大,导致服务不可用。
问题:试想一个场景,正常来说,某个直播间的相关信息都是会缓存到Redis或者服务器的本地缓存中(提一嘴,我们有些并发量极高的数据是直接缓存在服务器的本地缓存中的,而不是缓存在Redis,因为Redis对于服务器来说是属于第三方组件,连接是耗时的,无论如何也比不过本地缓存。当然服务器本地缓存数据就需要每一台机器都缓存一份数据,就是空间换时间的概念,这个根据实际业务来衡量来采取哪种方案),而此时假设前端有一个异常请求,请求一个不存在的直播ID,那么我们首先会去查缓存,然后缓存没有,我们就会去查DB。好了,问题来了,假设前端伪造了N个这样的请求,那是不是N个请求都要去查DB,那么DB怎么可能扛得住啊。我们之前线上就出现过这样的问题,被恶意攻击了,血泪教训啊。
解决方案:
解决缓存穿透问题有几种方案:
1、布隆过滤器。通过在前端部署一个布隆过滤器,可以预先判断该键很可能不存在于数据库中,然后将其拦截,从而避免对数据库发起无效请求。但是这玩意维护成本太高了,对于我们的业务来说,这很明显是还用不上的。感兴趣的同学可以去了解下,这里我就不细讲了。
2、对明显有问题的前端传参进行过滤,比如,假设直播ID是64长度的字符串,那么后端可以对不满足64位的请求直接给过滤掉,不处理。当然,这种小把戏自然是挡不住黑客的,人家搞一堆64位的字符串长度的直播ID不分分钟的事吗
3、设置空缓存,比如,虽然数据库中没有直播ID为“xxxx”的数据,但是在Redis中对其进行缓存(key=xxxx, value=null),这样当请求到达Redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上。不过得注意,设置这种空缓存的时间不能设置过长,不然有恶意请求的时候,这种空缓存也会引起Redis存储空间暴涨。这个就是我们常用来处理缓存穿透问题的有效方案。 demo样例如下:
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "123456",
DB: 0,
})
err := rdb.Set(ctx, "xxxx", "null", 5*60).Err()
if err != nil {
panic(err)
}
二、缓存雪崩
定义:当Redis中的大量key集体过期或失效,这时候如果有大量并发的请求来到,Redis就无法进行有效的响应,所有的查询都落在数据库上,就会出现缓存雪崩。
问题:假设一个场景,我们的直播Redis机器在晚高峰时候出现了宕机,然后进行了重启,那么此时大量的前端请求就会进来,都会去读Redis。但是我重启之后,所以key值都被干掉了啊,那么这时所有的请求查Redis都得不到响应(太惨了),都需要重建缓存。这时候就会触发传说中的缓存雪崩(机器崩,人也差不多崩了)。当然,这是比较极端的一种场景。还有另一种情况就是使用了主动缓存(通过脚本或者定时任务等去生成缓存),然后设置的时间很集中,出现同时过期的时候,也会出现缓存雪崩问题。
解决方案:
解决缓存雪崩问题有几种方案:
1、给缓存失效时间设置一些随机值,让缓存尽量的打散,不要集中在一个时间失效。其实大部分业务场景缓存时间都是打散的了,我们只需要使用的时候多留意一下即可。
2、使用Redis集群,当一台宕机之后,还有备用节点,可以将请求打到正常工作的节点中。
总的来说,缓存穿透和缓存雪崩问题带来的最终结果都是灾难性的,但是个人认为这两个在正常业务中,都是比较可控的,除非一些异常场景,例如被恶意攻击等行为。