1
头图
本文主要叙述缓存并发,缓存雪崩,缓存穿透的问题以及解决方案。

缓存并发

什么是缓存并发

场景:在你每天刷抖音,看微信短视频时,都会有一个评论列表,在评论列表中,查询评论的时候,会先去查询Redis缓存,如果有,就立即返回;如果没,就去数据库查询数据,接着更新缓存,返回数据。这时候,如果访问量非常多,有多个C端同时查询评论,Redis缓存又恰好没缓存数据,此时,多个C端就会同时去查询数据库。上述这种现象,就被称为缓存并发。

命中缓存(图片摘自网络)

缓存失效或宕机(图片摘自网络)

话说回来,缓存并发会带来什么危害
  • 让数据库的压力剧增,因为数据库抗不了高并发,流量再大一点,可以直接被打垮,对用户极其不友好,在如今互联网上,App应用的被替代率是很高的,稍有不慎,用户就会被其他App抢走,这对企业来说是灾难性的。我们可以知道,Redis这玩意儿,要么不出问题,一出问题,肯定是大问题。这也就是为什么要强调知其然知其所以然的原因。
既然我们已经知道缓存并发的的严重性,那如何解决
  • 要解决问题前,我们得分析问题为何会产生。絮叨一下,很多人无论是在面试时或者工作时,遇到问题,就立马改,或者说按照百度给的方法就套上去试,这个不行,试那个。这种解决问题的方式是不正确的。一般解决问题的方法论是:遇到报错——定位问题——分析问题——设计解决方案——解决问题。回到缓存并发问题上,我们分析一下“为什么出现缓存并发”。

为什么会缓存并发

  • 原因一:在高并发场景下,机器刚刚启动,且没预热缓存,缓存都为空,又没用分布式锁
  • 原因二:在高并发场景下,机器运行已运行一段时间,但缓存刚好失效,又没用分布式锁
  • 原因三:在高并发场景下,Redis宕机了

以上3种原因都会产生缓存并发,如有遗漏其他原因,欢迎各位同学在留言区补充。

知道原因后,我们可以对照原因来设计解决方案了。

解决方案

  • 针对原因一和原因二,方案 :分布式锁。这个允许一个线程去请求数据库,其他线程挂起,线程查完数据库后,更新缓存,其他线程去访问缓存,返回数据。至于分布式锁如何使用和其底层实现,其实有比较多的细节注意的,但这里不过多讲解,后续会陆续出对应的文章,如果你安耐不住寂寞,可以去谷歌一下。
  • 针对原因三,方案:Redis的高可用。说到高可用,无非就是主从结构+哨兵模式,或者Redis集群。(加链接)

分布式锁和Redis高可用,其实是预防措施,这些应该是事前工作。如果现在缓存并发确实已经发生了,能做的就是把这个模块服务的流量入口缩小,以免因这个模块服务流量过大把MySQL直接打死,从而导致其他模块服务受到影响。如果是Redis宕机了,先把流量入口缩小,然后赶紧重启机器。

当然,最好是事前做好Hystrix的限流、降级、拒绝服务等工作。限流和降级,限流,顾名思义就是限制流量,允许多少流量可以通过,比如每秒最多1000。超过1000后的请求,就走降级,比如返回一些默认值或者友情提示:目前系统繁忙,稍后重试。拒绝服务,则是最后一道防线,如果流量持续一段时间后,仍然很大,就直接拒绝服务,拒绝服务是为了保护Redis直接被流量一波带走,如果Redis服务被打死后,恢复是比较耗费时间的,而且也会因为流量一直很大,刚重启,流量一波又直接带走Redis,完成双杀,Double Kill。拒绝服务,会等流量小后,较快恢复回正常服务状态。拒绝服务是迫不得已之举。

当初大头菜初入互联网行业时,乳臭未干,什么也不懂,用Redis前,只知道Redis很厉害,很适合做缓存,关于Redis的注意事项,除了知道在生产环境不能用keys *命令外,其他一概不懂。当时的评论项目,因为流量特别大,并发贼高,查询评论接口就出现了缓存并发问题,幸好提前做了默认值回复和限流+降级措施。下面是用分布式锁解决缓存并发的关键代码:

  @Autowired
  private DistributionLock locker;
  
 //没缓存,查数据库,获取评论
  if (comment == null) {
      //加分布式锁,只允许一个线程去回源
      if(locker.trylock(Constants.QUERYCOMMENT+moduleType+resourceId)){
          try {
              comment = getDataFromRedis(moduleType, resourceId);
              if(comment == null){
                  //缓存没数据,去数据库查
                  comment = getDataFromMongoDB(moduleType, resourceId);
              }
          }finally {
              locker.unlock(Constants.QUERYCOMMENT+moduleType+resourceId);
          }
      }

分布式锁(图片摘自网络)

接下来,继续讲缓存穿透

缓存穿透

什么是缓存穿透

场景:查询评论的时候,如果直接查询id=-1的数据,那么在缓存中,没命中,又去数据库中查找,又没命中。上述这种情况,被称为缓存穿透。用通俗易懂的话来概括:就是查找一个一定不存在数据库的数据,就叫缓存穿透。

那缓存穿透有什么坏处呢
  • 如果流量大时,会直接打趴服务,造成服务不可用。

按照方法论的套路继续走

为什么会出现缓存穿透
  • 原因一,非正常请求,比如:黑客攻击,专门构造一些特殊格式的数据来请求,给系统造成巨大压力。
  • 原因二,正常请求,用户输错了数据。

解决方案

  • 方案一:缓存空对象:在请求时,先访问缓存,查不到数据,再去数据库查询,数据库也查不到对应的数据,返回null给客户端。且异步更新缓存(key,“null”),并加入短暂的过期时间。

缓存空对象(图片摘自网络)

  • 方案二:方案一其实有一个明显的缺点,就是如果请求的数据一定不存在时,那么这个时候,缓存就缓存一大堆无用的(key1,"null"),(key2,"null")。浪费内存。这个时候可以结合数据校验和布隆过滤器。

布隆过滤器(图片摘自网络)

说到数据校验,这个事儿是特别重要的。尤其是在一些直接涉及到钱的服务中,数据校验是巨重要的。如果不进行数据校验,大公司的老板就少一台宾利。小公司,就可能直接破产了。下面是大头菜的同学所在公司的案例
案例

案例-1

不管如何,培养良好的开发习惯,能让你受益终身。

缓存雪崩

什么是缓存雪崩

场景:在评论列表中,如果有一批评论成为了热点评论,但不幸,此时这批条评论,在Redis缓存中,都失效了,由于没命中缓存,加上大量请求,都去数据库查询评论,从而给数据库造成极大压力,甚至崩溃。这种情况,被称为缓存雪崩。
缓存雪崩(图片摘自网络)

缓存雪崩的坏处

给数据库极大压力,甚至打垮数据库,从而造成系统不能正常提供服务。

为什么会缓存雪崩

  • 原因一,redis宕机,相当于多个key同一时刻失效。
  • 原因二,redis没宕机,多个key正常到时就失效。
总结:如何你面试的时候,遇到这个问题,最好分情况回答,就是redis是否宕机。给面试官留下脑子清晰的印象。

解决方案

  • 方案一,解决原因一,既然宕机了,那就想到高可用,redis集群,哨兵模式,故障转移和故障恢复,同时还应该做好监控和报警。如果没法自动完成故障转移,那就人工干预。
  • 方案二,过期时间=失效时间+随机时间,解决原因二。
  • 方案三,永不过期,既然你是因为过期时间到了导致的雪崩,那就干脆让你不过期就完事了。有人会问,那缓存一致性怎么保证?后台主动更新:就是通过mysql更新的时候,让mq监听Binlog,回调更新缓存。使其缓存和数据库数据保持一致。
  • 方案四,可以从应用架构角度出发,通过限流,降级,熔断手段来降低影响,除此之外来避免多级缓存来避免这种灾难。如果你使用的微服务架构是SpringCloud,那你可以直接使用Hystrix,来实现限流,降级,熔断,修改一下配置文件即可。

补充

缓存击穿,就是只有一个key过期的缓存雪崩。

总结

全篇下来,几乎都是按照遇到报错——定位问题——分析问题——设计解决方案——解决问题方法论来写的。文中介绍了缓存并发,缓存穿透,缓存雪崩的定义,危险,原因,解决方案。大头菜希望你读完这篇文章后,不仅能学会知识,更能掌握定位问题——分析问题——解决问题的方法论。知识点,这些以后可能会忘,但方法掌握好后,方能见招拆招。

非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力!


大头菜
41 声望8 粉丝