Scott's world.

缓存策略和缓存故障

Word count: 2.6kReading time: 9 min
2019/12/01 Share

缓存策略和缓存故障

缓存策略能应用在很多需要提高系统性能的地方

我们引入缓存,通过缓存数据,而当从缓存里面存取数据即更新数据的时候,速度可以显著提高,当然在缓存数据之后还要考虑如何安全地同步到数据源

而缓存策略就是为了解决这一问题而出现的

下面我们会见到几种最常见的缓存策略,分别介绍他们的优缺点和使用场景

  • Cache-Aside
  • Read-Through
  • Write-Through
  • Write-Behind

同时我也会通过Redis出现介绍经常出现的缓存故障\

  • Redis雪崩
  • 缓存击穿
  • 缓存穿透

Cache-Aside

Cache-Aside应该是我们最常见也最常用的缓存策略.

在这种缓存策略下,应用程序(Application)会与缓存(Cache)和数据源(Data Source)进行通信

而最关键点的在于应用程序会在命中数据源之前先检查缓存

这里有一张图片能很好地说明这一问题,图片来自于于yanglbme

我们来通过这一张图片来看一下具体的过程

先从一次请求数据的过程

  • 首先,应用程序确定缓存中是否存有数据
  • 若缓存中有该数据,也即Cache hit(缓存命中),数据也就直接从缓存中读取并且返回给应用程序
  • 若数据不存在缓存中,也即Cache miss(缓存未命中),应用程序会从数据存储的地方读取该数据,比如说MySQL数据源,并将数据存入缓存中,最后返回给客户端应用程序

从上面请求数据来看,这种策略很明显应用于读多的场景,它的目的是想不经过数据源直接从缓存中读取数据,在一定程度上抵抗缓存故障,如果缓存服务器发生故障,系统仍然可以通过直接访问数据库进行操作

但是我们在流程中也注意到,这种策略并不能保证数据存储和缓存之间的一致性,比如你在读取缓存的时候并不知道该数据是否在数据源进行了更新即无法验证其缓存的有效性,所以需要配合使用其他缓存策略来更新或使缓存无效

另外,在首次请求数据时,若总是会导致缓存未命中,在这种情况下需要额外的时间来将数据加载到缓存中,类似达到了缓存击穿的效果

而为了解决这一问题,一般可以对数据进行”预热”的操作,减少数据加载到缓存中的时间,但是手动预热的可操作性不高,也解决不了可过期缓存的问题,所以也有用另一个缓存作为锁控制查询来解决,比如使用redis或者zookeeper来做互斥锁

Read-Through

Cache-Aside策略中我们看到应用程序需要与缓存和数据源进行通信

而在Read-Through策略下,应用程序无需管理数据源和缓存,只需要将数据源的同步委托给缓存提供程序Cache-Provider即可

即所有数据交互都是通过抽象缓存层完成的

上面的流程也与Cache-Aside类似,只不过通过抽象缓存层下,任何的操作都要经过缓存提供程序,比如在缓存未命中时从数据源返回的数据首先到缓存提供程序然后在返回给客户端应用程序的同时也要将此数据加载到缓存服务中

在进行大量读取的时候,此策略下可以减少对数据源上的负载,也对缓存服务的故障也具备一定的弹性,比如若缓存挂了,则Cache Provider仍然可以直接转到数据源来进行操作

然而,这样同时也有不少的问题

比如在首次请求数据的时候,还是会有缓存击穿的问题,解决思路与上述类似

Read-Through适用于较稳定的读多特别是多次请求相同数据的场景,虽然与Cache-Aside策略非常相似,但依旧有区别

  • Cache-Aside中,应用程序负责从数据源中获取到数据并更新到缓存
  • Read-Through中,此逻辑通常是由独立的缓存提供程序提供

Read-Through只是在`Cache-Aside之上进行了封装,可以方便配置不同的缓存服务

Write-Through

Write-Through策略,也会用到缓存提供程序即Cache-Provider,此策略下当发生数据更新(Write)下通过缓存提供程序来负责更新底层数据源和缓存.缓存与数据源保持一致,并且写入时始终通过抽象缓存层到达数据源

这里其实很明显地看出,如果更新操作要同时更新缓存和数据源,因此数据写入速度会降低

但是通过这一策略解决了Read-Through数据一致性问题,从而使缓存失效得到有效解决

Write-Behind

Write-Behind策略最大的特点就是在于引入了队列

如果没有强一致性要求,将缓存的更新请求入队,并且定期将其flush刷新到数据存储中,即在数据更新的时候直接写入缓存

如果在数据更新时直接写入缓存不考虑其他,可以增加数据写入的速度,适用写多的场景

而与Read-Throug配合比较适合用在混合工作负载,在缓存中数据总是可用有效

Redis雪崩

现在很多电商首页以及热点数据都会做缓存,而在考虑更新缓存时都是定时任务去刷新,或者是查不到之后去更新,而定时任务就有一个问题

举个简单的例子:若缓存中Key失效时间固定在某一时间点,在某个时间点,有一个秒杀活动时有大量用户涌入,假设当时每秒5000个请求,而缓存本可以抗住4000个请求每秒,但是缓存当时所有的Key都失效了,此时所有请求全部落在数据库上,数据库的结果,可能直接就挂了,如果你想重启数据库时,但是又有新的请求涌入,就又被打死了

这就是常见的缓存雪崩

简单来说在同一时间大面积失效的缓存下,高并发的请求下直接将数据库打爆,但是若是打爆一个的分库,而其他依赖它的库也会同时报错,如果没有做熔断等策略那基本上就全完了

解决

上面我们知道雪崩的关键在于大面积同时失效的Key,那是在Key失效时间都固定在同一时间下,所以我们应该从这里下手

那就是在Redis存数据的时候,把每个Key的失效时间都加个随机值即可

若是对于热点数据,那就直接设置热点数据永远不过期,在更新时直接更新缓存就好了

上面单机情况下的解决办法,但是Redis若是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题

缓存穿透

缓存穿透也就是请求在缓存和数据源中都没有的数据

比如常见的一种场景

我们的数据库id都是1开始自增开始,若用户不断发起请求的id值为-1的数据或id值特别大不存在的数据,这样会导致数据库压力过大,严重会击垮数据库

这里的图片参考于敖丙

img

这里就是不对参数做校验,每次都能绕过Redis直接到达数据库,数据库也查不到,这样的请求在高并发下就直接打挂数据库

解决

刚才我们也看到它是在查询即调用接口时

所以出发点就是在接口层增加校验

比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等

从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)

还有一个高级用法布隆过滤器(Bloom Filter,这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了数据库刷新KV再return

缓存击穿

这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了数据库

而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞

解决

前面我们也在缓存策略提到过

直接通过Zookper或Redis加上互斥锁或者设置热点数据永不过期

一般出现上诉情况发生,这里引用敖丙[https://juejin.im/post/5dbef8306fb9a0203f6fa3e2]老兄的原话

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

参考

https://juejin.im/post/5d9fe277518825192d46ef07

https://juejin.im/post/5dbef8306fb9a0203f6fa3e2#heading-4

CATALOG
  1. 1. 缓存策略和缓存故障
    1. 1.1. Cache-Aside
    2. 1.2. Read-Through
    3. 1.3. Write-Through
    4. 1.4. Write-Behind
    5. 1.5. Redis雪崩
      1. 1.5.1. 解决
    6. 1.6. 缓存穿透
      1. 1.6.1. 解决
    7. 1.7. 缓存击穿
      1. 1.7.1. 解决
    8. 1.8. 参考