每当我们讨论缓存时,总是会对如下几个词比较熟悉,

Write-back,  write-through, write-around

似乎,缓存主要是为“写”设计的,其实这是错误的理解,写从缓存中获得的好处是非常有限的,缓存主要是为“读”服务的。

之所以我们要顺带提一下,在一个缓存系统中,如何处理写的顺序,是因为,在写的过程中,需要动态的更新缓存(否则就会产生数据不一致性的问题),以及后端主存。这三个词都是用来表示如何处理写更新的。就是用什么方式来处理写。

在一个有缓存的层次结构中,如何理解缓存是为“读”服务的?这涉及到读请求的处理序列。对于每一个读请求,我们都会用如下的操作序列去处理它:

1.     在缓存中查找请求对应的数据

a.     如果找到,则直接返回给客户

b.     如果没找到,则把请求的数据读入缓存(更新缓存),然后把数据返回给客户

既然缓存主要是为读服务的(后面的文章,我们会讨论,用什么方式来改善写的性能),那么为了提高读的性能,或者说减少读的响应时间,我们就要提高缓存的命中率,减少缓存的miss 率。这也是我们缓存算法设计的目标。

那么我们来想想,在设计缓存时,我们应该从哪几方面考虑来达到这个缓存的设计目标呢?根据我们上面提到的读请求的操作序列,我们可以从如下几个方面来思考:

1.     我们应该尽量多的用有用的数据填满缓存。也就是说,我们要充分利用缓存。

a.     这是缓存模块和其它模块不同的地方,并不是说缓存中的数据越少越好,而是有用的数据越多越好。

b.     这里有个非常好的列子,就是Windows的内存占用率总是非常高,很多人都表示过不满。其实这是一个非常好的设计。Windows总是试图尽量利用那些空闲的内存,用来缓存磁盘上的数据,以此来提高系统的整体性能。否则你那么大的内存,就为了拿来好看?

2.     如何获取“有用”的数据。这里,“有用”的数据的定义就是可能在不久的将来会被client用到的数据。为了得到有用的数据,我们需要预估客户端应用的I/O 模式,比如顺序读写,随机读写等等。这里就涉及到了“pre-fetch”算法。

a.     Pre-fetch(预取算法):是一种预测客户端应用下次读写的数据在哪里的算法,并且把客户要用的数据提前放入缓存中,以此来提高读的响应速度。

3.     问题来了,如果缓存已经满了,那么如何存放新的需要缓存的单元呢?这就牵涉到缓存设计的另一端:淘汰算法。

a.     相比于pre-fetch,淘汰算法(replacement policy)更加重要。因为对于随机的I/O, 预取算法是无能为力的。

b.     淘汰算法的关键是如何判断一个单元的数据比另一个单元的数据更加重要。但需要淘汰一个数据单元时,丢弃掉最不重要的那个数据单元,并且用它来存放新的数据。

4.     缓存算法设计的另外一个重要的考虑因素是算法的复杂度。或者说是实现或运行算法带来的额外开销。我们希望算法容易实现,并且额外开销不随着缓存大小的改变而改变。包括容量的额外开销和计算的额外开销。

接下来的文章,我们会详细讨论预取算法和淘汰算法。