缓存的几种模式

缓存在现在的软件系统毫无疑问是非常重要的一环,缓存对于系统的性能瓶颈、用户感知
等都有着巨大影响。本文将记录缓存设计的几种常见模式,并且延伸到一些其他组件上。

缓存的几种模式
应用程序使用缓存来改善对某些信息的重复访问(避免访问持久层的数据库,从而对数据库造成过大压力),但是,保持缓存的数据与持久层数据的一致性是一个微妙的问题,应用程序应该实现某种确保缓存尽量是
最新的策略,也能检测和处理缓存中数据过时的问题。
通常,有如下几种模式:

Cache Aside模式

大部分后端系统使用的都是这种模式, 它的逻辑如下:

  • 失效 : 应用先从缓存取数据,如果没有取到则到更后面的持久层取-通常是数据库,
    成功后放到缓存中
  • 命中 : 应用直接从缓存中获取到需要的数据
  • 更新 : 先存储数据到持久化层中,再让缓存失效

cache aside

上面说道,对于更新是保存到数据库后让缓存失效,这是为什么呢?

当两个并发线程同时更新相同的数据库项时,”每次”都更新了缓存,这里会有两个问题:

  1. 在两次更新的中间时候,用户在缓存中读取的数据“并不一定”是数据库中的值(因为实际上数据库中被并发的修改),
    这里实际上已经是一个“脏”数据
  2. 如果并发写比较多的时候,这里每次都涉及到更新,但是这个缓存到底会不会被频繁访问? 如果没有多次更新
    就是浪费,而如果采用失效缓存,缓存也就是重新计算一次而已。
    相当于说,失效缓存-其实就是一个Lazy的思想,延迟加载数据已减轻系统压力

同时,上述模式是否就真的没有问题?考虑这种情况:

  • A和B两个线程, A读数据B写数据,同时发生
  • A从缓存中没有读到数据,从数据库读取
  • B写数据到库中,同时让缓存失效
  • A把从数据库中读取到的老数据放入缓存中
  • 缓存中是老数据,数据库中是新数据。出现缓存不一致

这种情况从逻辑上来说是完全可能发生的,Quara中有个[问题]
(https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend)
对这个也做了解答。如果要保证严格的一致性,要么采用顺序性的提交要么采用Paxos或者Raft之类的一致性算法,但很明显要么是性能影响过大要么是实现过于复杂。
而上面提到的corner case,其实发生几率应该是非常小的,因为查询后没有放到缓存且等到更新数据库并更新缓存过后才进去,时间节点上不大可能会出现, 因为更新总是比查询慢很多。

Read Through模式

read through其实就是在查询操作中自动更新缓存, 当缓存失效的时候,缓存系统自身从持久化系统(数据库)中查询数据,把该数据更新到缓存中然后返回前端。 整个过程对于缓存的调用方来说完全透明,从调用方的角度来看是最为简单的。 这种模式的原理非常简单,后端服务最为经典的案例有几个:

My Batis

mybatis默认的一级缓存隐藏在我们使用SqlSession后面,对于每一个SqlSession实例都自动缓存查询。但是对于mybatis的使用者来说我们并不需要关心缓存的键、过期时间、更新策略等。所以是一种典型的read through模式实现。我的这篇文章-%E4%B8%80%E7%BA%A7%E7%BC%93%E5%AD%98/)对这个有所描述

MySQL的存储引擎

对于数据库系统,一个很基本的问题是:

  • 是否所有的查询都会走到磁盘上?

这里的查询分为两类:

  • 实际查询SQL
  • 存储引擎内部通过索引查找数据的“索引”定位

这里是仅仅列出了MySQL,实际上所有的数据库都会有这种缓存存在。

CPU高速缓存

对于现代CPU的几级缓存,比如L1/L2/L3, 它们使用的模式就是read through, 低级的缓存对各自CPU核心负责,而最上层一级缓存则对整个CPU起到缓存作用 - 缓存来自主存的数据。 因为对于CPU来说,内存还是过于缓慢,所以为了处理CPU和内存之间速度上的差异在现代CPU上引入了这种缓存结构, 当然,他们也是易失性缓存

Write Through模式

write through模式同read through模式相仿, 只不过是在数据更新的时候如果没有命中缓存才更新数据否则就更新缓存,然后再由缓存自身更新到实际持久层(同步)
这种模式有如下的特点:

  • 所有数据都被缓存在内存中,所以查询效率较高
  • 持久化的数据不能太大,因为内存是有限的
  • 写数据时,缓存把数据持久化的过程需要是同步的才能保证可靠性

Write Behind Caching模式

也叫Write Back模式,就是上面的Write Through模式的异步版本:写数据到持久层的操作由缓存层自己异步的处理,根据某种策略异步持久化。那么,这种方式的特点就非常明显:

  • 写数据(磁盘IO)非常快
  • 缓存层可以对多次操作进行合并,进而可以较大的提高性能
  • 数据真正被持久化会有一定的延时,对于异常情况就可能丢失数据

在上述中可以看到优缺点都非常明显,数据不是强一致性的而且可能会丢失(对于某些已数据为唯一目的的系统比如数据库系统就是无法接受的),但是操作速度可以非常快。 所以说在软件工程上不存在完全完美的方案,大部分时候都是一个取舍的过程(trade off)
另外,这种模式还有问题:

  • 通过什么方式判断缓存是否存在(比如对于文件系统)
  • 持久化的时机(定时或者定量?)

cache aside

缓存系统设计的几个问题

在分布式系统中,毫无疑问我们需要一个集中式缓存集群来做统一的缓存处理,那么,对于这个缓存系统来说有几个问题小于要注意:

  • 内存是否足够
    如果内存不足以存下所有数据,那么需要对缓存的数据进行分片。同时,缓存系统的网络也需要足够好
  • 缓存持久化策略
    是基本不管持久化作为一个存粹的缓存层还是要提供最大程度的持久化?
  • 缓存的过期策略
    缓存数据存放多久? 放太久或者放太短都系统来说都是坏消息,最合理的可能是交给应用自行设置。
  • 缓存的淘汰策略
    当内存不够需要清除缓存时,是按照最近最少使用还是大小或者是其他方式?
  • 缓存系统的可用性
    如果缓存系统挂掉且系统突遇高峰流量-也就是俗称的缓存雪崩如何处理?如何来监控缓存系统的示例是否正常使用以及如果挂掉如果处置?