缓存雪崩、缓存穿透、缓存击穿 问题分析和解决
在应用中,如果使用缓存,我们主要有三个目的:
1、加快用户访问速度,提升用户体验
2、降低后端负载压力,减少潜在的风险,保证系统平稳运行
3、尽可能保证数据一致,即时更新
但是在使用缓存时,也会有很多问题可能出现,不是把数据根据Key存储到缓存服务,能正常查询就完工大吉了的。身为一个严谨的开发人员,必须要考虑各方面会出现或可能出现的问题,并对风险进行预处理,尽可能的保证程序应用的稳定和健壮性,下面对使用缓存时比较常见的问题和风险进行原因分析和常见的解决方案。
缓存穿透
缓存穿透指的是 查询一个根本不存在的数据,缓存层和存储层都不会被命中。我们正常的缓存使用大致流程是对数据的查询,先在缓存层进行查询,如果数据的Key已过期或者Key不存在,再从数据层进行查询,并且把查询到的数据放进缓存并设置过期时间,如果查询数据为空 则不放进缓存。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
而在这种情况下,我们在请求时 每次都传入错误的参数(根本不会存在的数据),每次查询都会去数据层查询,而且每次查询结果都为空,每次又都不会进行缓存,假如有恶意攻击,就可以利用这个漏洞对数据库造成压力,甚至压垮数据库。
通常我们可以在程序中分别统计总调用数,缓存层命中数,存储层命中数,如果发现大量存储层空命中,就可能是出现了缓存穿透问题。造成缓存穿透问题的原因有两个。
1、业务自身代码或者数据出现问题
2、一些恶意攻击、爬虫造成大量非法请求
解决方案
1、缓存空对象:在工作中,我们通常会采用缓存空值的方式,也就是如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。
但是缓存空对象也会有两个问题:
- 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
2、布隆过滤器拦截:在访问缓存层和存储层之前,将存在的 key 用布隆过滤器提前保存起来,做第一层拦截。
例如:一个智能推荐系统有几百万个用户ID,每小时会有算法根据每个用户的行为画像为其定制一套 个性数据列表(例:类似某易云-私人FM),但是如果最新的用户因为没有历史行为,就会发生缓存穿透的行为,那么此时可以将所有个性化推荐数据用户的数据做成布隆过滤器。如果布隆过滤器认为该用户ID不存在,那么就不会访问存储层,很大程度上保护了存储层。
布隆过滤器参考:Bloom Filter(布隆过滤器的概念和原理)
使用布隆过滤器的话,这种方式比较适用于数据命中不高,数据固定实时性低(通常是数据集较大)的应用场景,代码维护难度比较高,但是缓存空间占用比较少
缓存雪崩
缓存雪崩是指在某一个时间段,缓存集中过期失效或缓存服务宕机无法使用,导致所有请求的访问查询在一时间全部都落到了数据库上,对于数据库而言就会产生周期性的压力波峰。
解决方案
1、对于缓存集中过期失效的情况处理可以对同类型缓存数据,失效时间均匀分布,比如:在失效时间基础上再加1~10分钟的随机数时间
2、如果是缓存服务宕机 则需要考虑做主备或集群,和飞机的多个引擎一样,如果缓存服务设计成高可用的即使个别节点、个别机器宕掉。依然可以提供服务
3、无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部 hang 在这个资源上,造成整个系统不可用。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成前端页面是开天窗。
在实际项目中,我们需要对重要的资源 ( 例如 Redis、 MySQL、 Hbase、外部接口 ) 都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现了问题,对其他服务没有影响。但是线程池如何管理,比如如何关闭资源池,开启资源池,资源池阀值管理,这些做起来还是相当复杂的,这里推荐一个 Java 依赖隔离工具 Hystrix(https://github.com/Netflix/Hystrix),
缓存击穿
缓存击穿是指一个资源非常热点,在不停的扛着很多并发,很多并发请求集中对这一个点进行访问(例如:某浪热搜、某明星结婚新闻) ,而当这个缓存资源在失效的瞬间,持续的大量请求就穿破缓存直接请求到数据库,就想在堤坝上凿开了一个小洞
解决方案
解决该问题的思路主要目的是
- 减少重建缓存的次数
- 数据尽可能一致
- 较少的潜在危险
1、永远不过期:包含两层意思:从缓存层面,设置该资源永不过期,所以不会出现Key过期后产生的问题,也就是物理不过期。从功能上为每个Value设置一个逻辑过期时间,当发现逻辑过期时间后 使用单独的线程去构建缓存
2、mutex key互斥锁:此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,再重新获取缓存数据即可。即:该资源加锁 访问该资源时先判断该缓存是否已经有线程在重新构建,如果有其他的线程在构建该资源,等待构建线程执行完毕再进行访问。