楔子
在使用 Redis 时,会面临缓存雪崩、缓存穿透、缓存击穿等问题,无论哪一个发生,都会导致大量请求打到数据库。如果数据库宕机,那就是很严重的事故了。
下面我们就来分析一下,这几个问题产生的原因以及解决办法。
缓存雪崩
缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机。这种情况就叫做缓存雪崩。
以上对比图可以看出缓存雪崩对系统造成的影响,那么问题来了,缓存雪崩是如何产生的呢?
- 缓存中有大量 key 同时过期,导致相应的请求会打到数据库;
- Redis 实例宕机了;
而问题的解决方式也很简单,首先来看第一种情况。
1)当大量 key 同时过期时。
为了避免缓存同时过期,可在设置缓存时额外添加一个随机时间,这样一来数据的过期时间会有所差别,但差别又不会太大。即避免了大量的缓存同时失效,又能满足业务功能。
除了微调过期时间之外,还可以通过服务降级。而所谓的服务降级就是指,在服务器资源不够、或者说压力过大时,将一些非核心服务暂停,优先保证核心服务的运行。比如:
- 当业务应用访问的是非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;
- 当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取;
这样一来,只有部分过期数据的请求会发送到数据库,数据库的压力就没有那么大了。
另外还设计二级缓存,也就是除了 Redis 之外,再设置一层缓存,当缓存失效之后,先去查询二级缓存。
2)Redis 实例宕机。
实例宕机相比缓存雪崩要更加严重,一般来说一个 Redis 实例可以支持数万级别的请求处理吞吐量,而单个数据库可能只支持数千级别的请求处理吞吐量,它们两个的处理能力至少相差了近十倍。由于 Redis 缓存失效,所以数据库就可能要承受近十倍的请求压力,从而因为压力过大而崩溃。
这个时候,可以进行服务熔断。服务熔断指的是在发生缓存雪崩时,为了防止引发数据库雪崩,甚至是整个系统的崩溃,我们暂停业务应用对缓存系统的接口访问。再具体点说,就是业务应用调用缓存接口时,缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,等到 Redis 缓存实例重新恢复服务后,再允许应用请求发送到缓存系统。
这样一来,我们就避免了大量请求因缓存缺失,而积压到数据库系统,保证了数据库系统的正常运行。
在业务系统运行时,我们可以监测 Redis 缓存所在机器和数据库所在机器的负载指标,例如每秒请求数、CPU 利用率、内存利用率等。如果我们发现 Redis 缓存实例宕机了,而数据库所在机器的负载压力突然增加(例如每秒请求数激增),说明就发生缓存雪崩了,大量请求被发送到数据库进行处理。我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,从而降低对数据库的访问压力。
因此服务熔断虽然可以保证数据库的正常运行,但是暂停了整个缓存系统的访问,对业务应用的影响范围大。为了尽可能减少这种影响,我们也可以进行请求限流。也就是在业务系统的请求入口前端,通过加锁排队的方式控制每秒进入系统的请求数,避免过多的请求被发送到数据库。
假设业务系统正常运行时,请求入口前端允许每秒进入系统的请求是 1 万个,其中 9000 个请求都能在缓存系统中进行处理,只有 1000 个请求会被应用发送到数据库进行处理。
然而一旦 Redis 宕机,数据库的每秒请求数会突然增加到每秒 1 万个,此时我们就可以启动请求限流机制,在请求入口前端只允许每秒进入系统的请求数为 1000 个,再多的请求就会在入口前端被直接拒绝服务。所以使用了请求限流,就可以避免大量并发请求压力传递到数据库层。
所以使用服务熔断或是请求限流机制,来应对 Redis 实例宕机导致的缓存雪崩问题,是属于事后诸葛亮。也就是已经发生非常严重的缓存雪崩了(实例宕机了),我们使用这两个机制,来降低雪崩对数据库和整个业务系统的影响。而我们也可以提前预防,也就是通过主从复制的方式,搭建 Redis 高可用集群,主节点挂了就切换到从节点。
所以当发生缓存雪崩时,解决方案如下:
- 随机化过期时间;
- 服务降级;
- 设置二级缓存;
- 服务熔断(Redis 实例宕机,问题很严重了);
- 请求限流(相比服务熔断,限流的影响要小一些,它还允许一部分请求过来,交给数据库来处理);
- 搭建 Redis 集群;
缓存击穿
缓存击穿指的是热点数据在某一时刻失效了,然后有大量的并发请求要访问热点数据,但由于数据已失效,于是这些请求就会全部打到数据库,从而给数据库造成巨大的压力,这种情况就叫做缓存击穿。
缓存击穿的执行流程如下图所示:
它的解决方案有以下两个:
1)加锁排队
此处理方式和缓存雪崩加锁排队的方法类似,都是在查询数据库时加锁排队,以此来减少服务器的运行压力。
但缓存击穿只是热点数据失效,所以我们有更加优雅的方式解决。
2)永不过期
对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性。但需要注意:在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
缓存穿透
缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中。因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。
那么缓存穿透会在什么时候发生呢?
- 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
- 恶意攻击:专门访问数据库中没有的数据;
缓存穿透会给数据库造成很大的压力,而缓存穿透的解决方案有以下几个。
1)缓存空值或缺省值
一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值(例如,库存的缺省值可以设为 0)。后续应用发送请求进行查询时,就可以直接从 Redis 中读取空值或缺省值,然后返回。从而避免把大量请求发送给数据库处理,保证了数据库的正常运行。
但为了提高前台用户的使用体验 (解决长时间内查询不到任何信息的情况),但是我们可以将空结果的缓存时间设置得短一些,例如 3~5 分钟,以防止无用数据过多。
2)使用布隆过滤器
关于布隆过滤器我们后面会说,总之它的特点就是:如果布隆过滤器检测数据存在,那么数据有可能不存在;但如果布隆过滤器检测数据不存在,那么数据一定不存在。
如果数据不存在,那么就不会查询数据库了,这样一来即使发生缓存穿透,也不会影响数据库。布隆过滤器可使用 Redis 实现,本身就能承担较大的并发访问压力。
3)在请求入口的前端进行检测
缓存穿透的一个原因是有大量的恶意请求访问不存在的数据,所以一个有效的应对方案是在请求入口前端,对业务系统接收到的请求进行合法性检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉,不让它们访问后端缓存和数据库。这样一来,也就不会出现缓存穿透问题了。
跟缓存雪崩、缓存击穿这两类问题相比,缓存穿透的影响更大一些。从预防的角度来说,我们需要避免误删除数据库和缓存中的数据;从应对角度来说,我们可以在业务系统中使用缓存空值或缺省值、使用布隆过滤器,以及进行恶意请求检测等方法。
缓存预热
再补充一下缓存预热,首先缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。
缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,节约用户的等待时间。
缓存预热的实现思路有以下三种:
- 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据;
- 把需要缓存的方法挂载到某个页面或后端接口上,手动触发缓存预热;
- 设置定时任务,定时自动进行缓存预热;
小结
缓存雪崩、缓存击穿、缓存穿透三者都比较类似,缓存雪崩是大量的 key 同时失效,导致请求全部访问数据库;而缓存击穿是某个 key、只不过是热点 key 失效了,同样导致大量请求访问数据库;缓存穿透是大量请求访问不存在的 key,导致数据库压力增大。
因此这三者是比较相似的,它们的解决方案如下:
最后再说一下,服务熔断、服务降级、请求限流这些方法都是属于有损方案,在保证数据库和整体系统稳定的同时,会对业务应用带来负面影响。
例如使用服务降级时,数据的部分请求就只能得到错误返回信息,无法正常处理。如果使用了服务熔断,那么整个缓存系统的服务都被暂停了,影响的业务范围更大。而使用了请求限流机制后,整个业务系统的吞吐率会降低,能并发处理的用户请求会减少,会影响到用户体验。
所以尽量还是提前做好准备,防患于未然。
本文参考自:
- 极客时间蒋德钧:《Redis 核心技术与实战》