缓存系列:缓存穿透的解决思路

大家好,我是老黑。

上次我们讨论了在分布式系统下的缓存架构体系,从浏览器缓存到客户端缓存,再到CDN缓存,再到反向代理缓存,再到本地缓存,再到分布式缓存。整个链路中有非常多的缓存。

如果面试官问你说:给你一个机会,你有什么办法能够绕过这么多层缓存,将我们的慢设备击垮呢?(这里的慢设备一般指基于磁盘进行存储的关系型数据库,如MySQL)

难道这就是传说中的:「即使敌众我寡,末将也能从百万丛军中取敌将首级!」

这便是我们今天的主题:缓存穿透

什么是缓存穿透?

我们知道,缓存的工作原理是先从缓存中获取数据,如果有数据则直接返回给用户,如果没有数据则从慢速设备上读取实际数据并且将数据放入缓存。同步缓存就像这样:

或者使用异步的方式去同步缓存就像这样:

但是,如果缓存中和慢设备中一直都没有数据的话,是什么流程呢?

没错,如果缓存和数据库都没有的数据,请求仍然会先经过缓存再经过数据库,下次请求依然是同样的路径,这便是缓存穿透。

小结:缓存穿透,是指请求每次都要经过缓存,去访问慢设备,而慢设备又没有数据,在并发访问的情况有可能将慢设备击垮。

缓存穿透可能带来哪些问题?

我们知道了缓存穿透的场景后,其次我们要了解它的痛点是什么?

  1. 在高并发的情况下有可能将慢设备击垮;
  2. 每次请求都要经过无效的缓存层,请求相对较慢;

缓存穿透的解决思路有哪些?

问题1的解决思路:

高并发会将慢设备击垮,那么我们在这里的解决思路可以是让高并发变成低并发,让慢设备变得足够强大可以抗住足够的压力不被击垮。

高并发转换为低并发

如何让高并发变成低并发呢?

此刻你的思路没错,加锁。这种情况下可以是本地锁和分布式锁。本地锁指的是一个服务只允许N个线程同时访问慢设备(N代表N个实例),分布式锁指的是在永远只允许一个线程同时访问慢设备。当然了,也可以使用一些限流算法来玩。比如计数器法、令牌桶算法、漏桶算法等,我们今天的主题并不是它,暂且跳过,后续会有专门的文章来讲解。

回到正题。

服务实例数量不多的话可以使用本地锁,毕竟本地锁比分布式锁快。

服务实例数量较多的话则需要使用分布式锁来解决,进行流量防护。

但是如果数据一直不存在的话,仍然是有这种无效的请求,所以,在这一个线程访问慢设备的时候,如果数据为空,我们就约定一个数据放入缓存,如果数据为约定数据,则下次请求直接返回没有数据即可。示例图如下:

(这张图建议好好理解)

提高慢设备高可用

慢设备我们一般指的是DB,我们在这个场景中如何提升DB的高可用而不被击垮呢?

我们有以下两种思路:

我们在DB侧使配置最大线程数,用固定线程数对其进行限流处理;

或者是我们将DB做成主从数据库,读数据均匀的散布在从库上,并且从库让我们可以无限水平扩容。

问题2的解决思路:

问题2:每次请求都要经过无效的缓存层,请求相对较慢。

没错,细心的你一定发现了,我们在讨论《高并发转换为低并发》其实就已经解决了这个问题。

另外一种思考方式:

为什么会存在这个情况?(缓存无数据,慢设备无数据,但是又存在这样的请求)

我的猜想有:

  1. 攻击者!
  2. 数据被修改或删除,数据变更后,原来的查询路径无法获取数据;
  3. 慢设备故障导致数据丢失+缓存失效

那么如果是攻击者的话,我们的应对办法可以是请求达到阈值后封禁IP和对查询内容进行过滤,比如id≤0一定不存在则可以过滤。

如果是数据更新导致原路径无法查询,再加上高并发导致慢设备宕机,所以这种情况的应对办法应该是尽可能的减少热点数据更新频率+热点数据异步刷新保证热点数据在缓存中不过期。

如果是慢设备故障的话,那就没办法了,超出程序员应该有的思考范围了,这种情况下应该是做同城双活、异地多活等架构来保证高可用性。像阿里的两地三中心,三地五中心等架构,别着急,后面会详细介绍的,点个关注吧。

总结

缓存穿透是指:缓存和慢设备都没有数据,加上高并发的请求,慢设备扛不住导致宕机。

缓存穿透的解决方案:

  1. 如果是攻击者,封禁IP
  2. 提前做数据过滤,减少慢设备访问次数
  3. 当缓存无数据时,访问慢设备之前获取一把锁,减少慢设备并发访问
  4. 做好高可用架构设计

好了,本期就讨论到这里,感谢阅读。

既然都看到这里了,你的关注+点赞就是对我最大的支持。或者也可以关注我的公众号:老黑的博客。再次感谢!

猜你喜欢

转载自juejin.im/post/7074601904277291015