缓存

255 阅读7分钟
**缓存(Cache)**
 In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere. A cache hit occurs when the requested data can be found in a cache, while a cache miss occurs when it cannot. Cache hits are served by reading data from the cache, which is faster than recomputing a result or reading from a slower data store; thus, the more requests that can be served from the cache, the faster the system performs.

To be cost-effective and to enable efficient use of data, caches must be relatively small. Nevertheless, caches have proven themselves in many areas of computing, because typical computer applications access data with a high degree of locality of reference. Such access patterns exhibit temporal locality, where data is requested that has been recently requested already, and spatial locality, where data is requested that is stored physically close to data that has already been requested.

在计算机世界里,缓存是存储数据的硬件或者软件应用,可以更快的处理数据请求。存储的数据的可能是计算结果,也可能是副本。缓存命中是指请求的数据在缓存里面,缓存未命中就是请求的数据没在缓存。缓存命中时直接中缓存读取数据,比重新计算结果,或从原本的存储中获取更快,所以命中率越高,系统性能越高。
为了降本增效,缓存只能相对小一些。虽然有成本限制,但是在很多有良好局部性的计算机领域已经证明了缓存的价值。这种访问模式展现出时间局部性与空间局部性,时间局部性指请求的数据是近期访问过的数据,空间局部性指请求的数据与访问过的数据位置接近。

更快是缓存的核心优势,我们会想把访问的数据尽可能的放在缓存上。更小是缓存的关键限制,我们不得不面临,缓存很可能放置不下我们需要的全部数据的现实问题。缓存构建者需要提供合理的淘汰策略,尽量的保留缓存中的热点数据,保证命中率。缓存使用者需要从业务角度识别热点数据,结合缓存容量,判断哪些数据可以使用缓存,因为如果数据量远大于缓存容量,那缓存构建的再好,命中率也好不到哪去。分布式缓存的存在大大缓解了缓存容量的限制问题,但是更大的缓存容量无疑需要更高的硬件成本。

缓存加载

缓存中的数据不会凭空出现,需要使用者加载进去。加载时机一般有读取时未命中加载,服务启动时加载。读取加载的优势是不会拖累服务启动,加载的数据更接近真实使用的热数据。服务启动时加载的优势是,不会有首次访问效率问题。读取加载是更常用的加载时机。

缓存更新

数据一般不会一成不变,在数据发生变化的时候,需要调整缓存内容。不管缓存内容是数据副本还是计算结果,我们都可以认为有源数据与缓存数据两份数据,当数据存在两份时,首当其冲要面临的就是数据一致性问题。那么当数据发生改变时,我们应该如何处理源数据与缓存数据?下面我们在源数据存储在DB的前提进行讨论。

先写DB,后写缓存

上图是先写DB,后写缓存。这种模式下存在的风险是步骤3失败时(此时没有步骤4,或者步骤4返回失败),DB跟Cache会出现数据不一致的问题。

先更新缓存,后写DB

上图是先更新缓存,后写DB。这种模式下存在的风险是,如果步骤5失败,那步骤3、4获取到的数据就是错误数据,且直到该数据的缓存被修正前,所有从缓存读取到的都是错误数据。

先删缓存,后写DB

上图是先删缓存,后写DB。这种模式下存在的风险是,如果步骤5-8发生在步骤2与3之间,则虽然做了删除缓存的操作,但是又把历史数据加载到了缓存里面,且直到该数据的缓存被修正前,所有从缓存读取到的都是错误数据。如果步骤1成功,而步骤3失败,则只是加了缓存未命中的情况,对数据正确性没有影响。

先写DB,后写缓存中,写缓存没有区分是更新缓存还是删除缓存,是因为无论更新还是删除,存在的风险是一样的。

综合考虑,笔者认为先删缓存,后写DB,再删缓存的方式更加合适。毕竟删除缓存与写DB操作之间的间隙,可以通过一些手段控制到很小,降低中间插入因读取加载缓存出错的概率,然后再增加一次写DB后的删除缓存,来修正可能出现的错误。虽然第二次删除可能失败,但毕竟是小概率事件,而间隙中插入加载缓存操作与第二次删除缓存操作失败,两个小概率事件同时出现的可能性会更低。

缓存经典问题

使用缓存问题方面有经典的三个问题:缓存穿透、缓存击穿、缓存雪崩。下面我们介绍着三个问题产生的原因,以及解决方案。

缓存穿透

产生原因:高并发查询不存在的key值,导致请求大量落到DB。

解决方案:笔者认为这是从整个系统层面去考虑的问题,单单从缓存去考虑并不可能完整解决问题。比如要有监控,发现到DB的流量异常时,进行限流或者服务降级。 本文内容是缓存,这里我们局限在缓存范围内,看在缓存可以做什么文章(后面的缓存穿透与缓存雪崩也是只从缓存层面来考虑解决方案)。

如果要应对的场景不是攻击,而是正常业务,那可以把key值放入缓存,其value值为null,并设置一个较短的失效时间。如果面临的是外部攻击,那很可能key大多数是不重复的,这样除了额外增加了缓存的更新,不会有任何正向效果。

如果从缓存这一层就要做到保护系统,起到防护作用,那可以在缓存加一层布隆过滤器。将DB中存在的数据key值都放入布隆过滤器,每次数据请求先访问布隆过滤器,不存在就不需要访问缓存跟DB了。这个方式复杂度较高,需要从框架或平台层面支持,否则业务侵入太大。

缓存击穿

产生原因:非常热的key失效时,瞬间有大量请求打到DB。

解决方案:缓存加载时,先使用setnx锁定key,锁定成功的再从DB获取数据加载到缓存。

缓存雪崩

产生原因:批量热点数据同一时间段内失效,或者缓存层直接不可用,导致大量请求落到DB。

解决方案:针对批量热点数据同一时间段内失效的,可以给数据设置随机失效时间,避免出现这个问题。对于缓存层直接不可用的,需要将缓存设计为高可用架构,比如redis的哨兵模式。

🏆 技术专题第八期 | 聊聊缓存的妙用和问题