vlambda博客
学习文章列表

架构系列四(缓存方案架构设计思考)

1.引子

我们都知道,在一个应用系统中,存储层非常重要,它负责整个应用系统数据的持久化,比如关系型数据库oracle、mysql是很多业务系统存储层选型的不二之选。

另外我们还知道,存储层往往容易成为系统性能的瓶颈,持久化需要经过文件系统,并进一步读写磁盘,从系统的角度,IO操作是成本很高的一个事。

因此很多时候,我们在设计系统架构方案的时候,会结合业务考量,在应用层,与存储层之间增加一个中间层:缓存层。用于提升应用的响应能力,并有效降低存储层的负载。我们来看一个对比图:

从图上我们看到,左边是无缓存架构方案,右边是增加缓存层架构方案

  • 通过缓存层,应用层会优先访问缓存层获取数据,如果缓存层获取到目标数据,从缓存层直接返回

  • 如果缓存层未命中目标数据,则访问存储层获取数据。获取到数据后,从存储层返回,且将数据写入缓存层

  • 这里你可能在想,为什么增加了一层缓存层,架构复杂度不是更高了吗?怎么就性能更高了呢?这是因为缓存层操作的是内存,存储层是磁盘,访问内存的性能远远高于访问磁盘

增加缓存层,提升了应用的响应能力,并有效降低后端存储层负载。但是在缓存架构设计方案中,需要我们考虑的地方还是比较多的,有

  • 哪些业务数据适合缓存架构方案

  • 缓存粒度

  • 缓存一致性

  • 缓存穿透

  • 缓存雪崩

  • 缓存热点key

下面我们一起逐个来分析,让我们开始吧!


2.案例

2.1.哪些业务数据适合缓存

虽然缓存纵有千般好,我们切不可拿来主义,一定需要结合业务自身的需要。那么哪些数据适合缓存方案呢?

  • 不适合缓存方案:实时性要求高的业务数据,不适合采用缓存方案。比如说交易类数据订单、支付

  • 适合集中式缓存:更新频率不高,读多写少类业务数据,适合采用集中式缓存方案。比如说用户数据、商品数据

  • 适合本地缓存:非常稳定类业务数据,且数据量级不大,适合采用本地缓存方案。比如说配置、参数类数据

你看区分业务数据是否适合采用缓存方案,我们主要有两个考虑的点

  • 实时性:实时性要求高的业务数据,不适合缓存;反之则适合缓存方案

  • 数据量级:在采用缓存方案的时候,如果数据非常稳定,且数据量级不大,我们还可以考虑直接使用本地缓存方案。通过本地缓存减少网络调用,进一步提升应用响应能力


2.2.缓存粒度

关于缓存粒度,我们知道增加缓存层方案,本质上是通过空间换取时间的方案。

即牺牲更多的存储空间,从而提升应用的响应能力。因此我们在设计缓存方案的时候,需要结合业务需要,处理好缓存粒度,避免过渡浪费存储空间。举个例子

  • 不要把select * from xxx的数据都放入缓存

  • 而是结合业务需要,明确业务字段select a,b,c from xxx放入缓存


2.3.缓存一致性

缓存一致性问题,是缓存架构方案中非常重要,又需要慎重的一个问题。这里说的一致性问题,即存储层、缓存层数据的一致性问题,涉及到缓存的更新。缓存更新实际应用中可以结合缓存淘汰、超时过期、主动更新实现

当然我们在系统中使用缓存架构方案的时候,通常业务上可以允许一定时间内的数据不一致,但这不是说可以放纵数据不一致。那么当存储数据更新以后,如何处理缓存层的数据,从而保持一致呢?

结合业务需求,我们可以去思考这么几个点,以下是主动更新的策略

  • 更新策略:更新存储层,更新缓存层;或者更新缓存层,更新存储层

  • 删除策略:更新存储层,删除缓存层;或者删除缓存层,更新存储层

你看这里有两种策略可以选择,具体哪一种更优呢?首先我们来看更新策略,这是一种不推荐的方案,理由是更新成本往往更高

  • 也许放入缓存中的数据,需要经过一定的计算,计算成本高

  • 也许连续更新多次,且这段时间没有读请求,事实上缓存中需要存储最近一次更新的结果,那么前n-1次更新都是浪费

分析了更新策略不是推荐方案后,我们再来看删除策略,这是实际应用中推荐的方案,理由是删除策略比起更新策略,陈本更低

  • 删除动作,比更新动作更轻,没有计算成本

  • 连续多次更新存储层,缓存层都是一个轻量级的删除操作,不存在浪费n-1次更新的问题

  • 缓存层删除后,等到接收到读请求,再从存储层获取数据--->计算--->放入缓存层


2.4.缓存穿透

关于缓存穿透,首先我们需要明确什么是缓存穿透?我们说当应用缓存架构方案以后,获取数据的流程是这样的:应用层--->缓存层--->存储层

当发生这样的情况,请求从应用层打进来,从缓存层获取不到目标数据,从存储层也获取不到数据。导致每次请求都直接打到了存储层,从而失去了缓存层降低存储负载的目的,更有甚者存储压力过大,最终系统崩溃!尤其是遭遇恶意攻击就更加麻烦了!

因此在缓存加过方案中,缓存穿透是一个大问题。那么有什么方案可以解决缓存穿透吗?有

  • 存储空值策略:存储空值方案,具体是说如果请求从缓存层没有获取到数据,进一步从存储也没有获取到数据。这个时候我们可以存储一个null值到缓存中,那么下一次同样的请求只需从缓存存直接返回null值即可,避免将请求再次打到存储层。

  • 布隆过滤器策略:如果业务需要缓存的数据,有明确的业务边界,那么我们可以在缓存层前,增加一个布隆过滤器过滤处理所有的业务key,避免非法的业务请求,尤其是恶意攻击。


2.5.缓存雪崩

缓存雪崩,是指当缓存系统不可用,请求都直接打到存储层,从而存储层因为压力过大产生故障。

可见缓存雪崩有两个特点

  • 主要原因是缓存系统不可用

  • 进一步导致存储层崩溃

因此,在设计缓存架构方案的时候,为了避免缓存雪崩的发生,缓存系统需要考虑

  • 高可用

  • 容错降级


2.6.缓存热点key

所谓热点key,是指这么一种场景。由于存在缓存过期的问题,比如说某个业务数据key触发了超时过期,需要重建缓存。

正常情况下

  • 一个读请求过来

  • 发现从缓存层拿不到数据,则从存储层获取数据

  • 将数据放入缓存层

这没有什么问题。但是要是恰巧

  • 多个读请求同时过来,并发问题

  • 大家发现从缓存层拿不到数据,就都从存储层获取数据

  • 获取到数据后,都将数据放入缓存层

这有什么问题呢?这会导致并发请求直接打到存储层,存储层压力过大;另外多个并发读请求重复处理缓存重建,浪费资源,极端情况下可能还会发生死锁。

因此缓存架构方案中,热点key的问题我们也不得不去考虑。实际应用中,该如何实现呢?我们来看一段伪代码

public <T> T get(String key){ T t = cache.get(key); if(t == null){ if(lock.tryLock()){ // 1.从存储层获取数据 // 2.将数据放入缓存层 // 3.释放锁 }else{ // 1.休眠等待sleep(50) // 2.再次调用get方法获取缓存 get(key); } } return t;}