概述

在我们的平常的项目中多多少少都会使用到缓存,因为一些数据我们没有必要每次查询的时候都直接查数据库或者调用第三方接口。特别是出现内存数据库之后,使用缓存场景更多了。而对于高 QPS 的系统尤为如此,如果每次都去查数据库,对数据库来说将是灾难。使用缓存业务系统一般的流程如下:

使用缓存机制的应用系统流程

问题汇总

使用缓存遇到的问题汇总如下:

缓存雪崩

概念

由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

三种处理办法:

  • 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
  • 为 key 设置不同的缓存失效时间:在一个基础的时间上加上或者减去一个范围内的随机值
  • 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。具体:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。
    另外一个更为接地气的方案
  • 事前:使用集群缓存,保证缓存服务的高可用
    在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。
  • 事中:ehcache本地缓存 + Hystrix限流&降级,避免MySQL被打死
    使用 ehcache 本地缓存的目的也是考虑在Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵。
    使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。
    然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
  • 事后:开启Redis持久化机制,尽快恢复缓存集群

缓存穿透

概念

指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。【只要是空的数据,查缓存又查db。造成数据库压力】

处理方案

  • 提供一个能迅速判断请求是否有效的拦截机制,比如布隆过滤器
    将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存 储系统的查询压力。【先看一下bitmap中有木有,没有直接返回;有就查缓存】
  • 简单粗暴的方法
    如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。【注意:过期时间要短,否则会缓存大量不存在key的数据】
  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。【吞吐量明显下降】
  • 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

缓存预热

概念

系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!【小量常用的数据】

缓存更新

概念

缓存更新除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰策略。

处理方案

  • 定时去清理过期的缓存;
  • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

备注:Redis采用如下策略进行数据淘汰:

  • noeviction:客户端若要使用更多的内存,直接返回错误(主要是write)
  • allkeys-lru: 将最近未使用key删除
  • volatile-lru:在 expire 集合中,将最近未使用的key删除
  • allkeys-random: 随机删除key
  • volatile-random:在 expire 集合中随机删除key
  • volatile-ttl: 在 expire 集合中,优先删除ttl 最短的key

缓存降级

概念

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。

处理方案

  • 系统可以根据一些关键数据进行自动降级
  • 可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的 (如加入购物车、结算)。
【保证服务的可用性】

缓存击穿

概念

在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。

处理方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库,在把结果放入缓存,后续请求直接拿缓存。没得到锁,则休眠一段时间重试。

参考资料

  • 为什么我们做分布式使用 Redis?
  • 乔二爷:阿里一面:关于【缓存穿透、缓存击穿、缓存雪崩、热点数据失效】问题的解决方案
  • 云枫随笔:Redis中数据淘汰算法总结
  • 也输:【存储系统架构设计】缓存穿透,缓存击穿,缓存雪崩,热点数据集中失效,redis 缓存系统四连击怎么解决?
  • 猿人课堂:关于Redis中缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案