[转] 通俗讲解:缓存、缓存算法和缓存框架

通俗讲解:缓存、缓存算法和缓存框架

 

 
1引言

咱们都听过 cache,当你问他们是什么是缓存的时候,他们会给你一个完美的答案,但是他们不知道缓存是怎么构建的,或者没有告诉你应该采用什么标准去选择缓存框架。在这边文章,咱们会去讨论缓存,缓存算法,缓存框架以及哪一个缓存框架会更好。html

 

2面试

“缓存就是存贮数据(使用频繁的数据)的临时地方,由于取原始数据的代价太大了,因此我能够取得快一些。”前端

这就是 programmer one (programmer one 是一个面试者)在面试中的回答(一个月前,他向公司提交了简历,想要应聘要求在缓存,缓存框架,大规模数据操做有着丰富经验的 java 开发职位)。java

programmer one 经过 hash table 实现了他本身的缓存,可是他知道的只是他的缓存和他那存储着150条记录的 hash table,这就是他认为的大规模数据(缓存 = hashtable,只须要在 hash table 查找就行了),因此,让咱们来看看面试的过程吧。面试

面试官:你选择的缓存方案,是基于什么标准的?算法

programmer one:呃,(想了5分钟)嗯,基于,基于,基于数据(咳嗽……)数据库

面试官:excese me ! 能不能重复一下?数组

programmer one:数据?!浏览器

面试官:好的。说说几种缓存算法以及它们的做用缓存

programmer one:(凝视着面试官,脸上露出了很奇怪的表情,没有人知道原来人类能够作出这种表情  )数据结构

面试官:好吧,那我换个说法,当缓存达到容量时,会怎么作?

programmer one:容量?嗯(思考……hash table 的容量时没有限制的,我能任意增长条目,它会自动扩充容量的)(这是 programmer one 的想法,可是他没有说出来)

 

面试官对 programmer one 表示感谢(面试过程持续了10分钟),以后一个女士走过来讲:谢谢你的时间,咱们会给你打电话的,祝你好心情。这是 programmer one 最糟糕的面试(他没有看到招聘对求职者有丰富的缓存经验背景要求,实际上,他只看到了丰厚的报酬  )。

 

3说到作到

programmer one 离开以后,他想要知道这个面试者说的问题和答案,因此他上网去查,programmer one 对缓存一无所知,除了:当我须要缓存的时候,我就会用 hash table。

在他使用了他最爱的搜索引擎搜索以后,他找到了一篇很不错的关于缓存文章,而且开始去阅读……

 

4为何咱们须要缓存?

好久好久之前,在尚未缓存的时候……用户常常是去请求一个对象,而这个对象是从数据库去取,而后,这个对象变得愈来愈大,这个用户每次的请求时间也愈来愈长了,这也把数据库弄得很痛苦,他无时不刻不在工做。因此,这个事情就把用户和数据库弄得很生气,接着就有可能发生下面两件事情:

1.用户很烦,在抱怨,甚至不去用这个应用了(这是大多数状况下都会发生的)

2.数据库为打包回家,离开这个应用,而后,就出现了大麻烦(没地方去存储数据了)(发生在极少数状况下)

  

5上帝派来了缓存

在几年以后,IBM(60年代)的研究人员引进了一个新概念,它叫“缓存”。

 

6什么是缓存?

正如开篇所讲,缓存是“存贮数据(使用频繁的数据)的临时地方,由于取原始数据的代价太大了,因此我能够取得快一些。”

缓存能够认为是数据的池,这些数据是从数据库里的真实数据复制出来的,而且为了能别取回,被标上了标签(键 ID)。太棒了

programmer one 已经知道这点了,可是他还不知道下面的缓存术语。

命中:

当客户发起一个请求(咱们说他想要查看一个产品信息),咱们的应用接受这个请求,而且若是是在第一次检查缓存的时候,须要去数据库读取产品信息。

若是在缓存中,一个条目经过一个标记被找到了,这个条目就会被使用、咱们就叫它缓存命中。因此,命中率也就不难理解了。

Cache Miss:

可是这里须要注意两点:

1. 若是还有缓存的空间,那么,没有命中的对象会被存储到缓存中来。

2. 若是缓存慢了,而又没有命中缓存,那么就会按照某一种策略,把缓存中的旧对象踢出,而把新的对象加入缓存池。而这些策略统称为替代策略(缓存算法),这些策略会决定到底应该提出哪些对象。

存储成本:

当没有命中时,咱们会从数据库取出数据,而后放入缓存。而把这个数据放入缓存所须要的时间和空间,就是存储成本。

索引成本:

和存储成本相仿。

失效:

当存在缓存中的数据须要更新时,就意味着缓存中的这个数据失效了。

替代策略:

当缓存没有命中时,而且缓存容量已经满了,就须要在缓存中踢出一个老的条目,加入一条新的条目,而到底应该踢出什么条目,就由替代策略决定。

最优替代策略:

最优的替代策略就是想把缓存中最没用的条目给踢出去,可是将来是不可以被预知的,因此这种策略是不可能实现的。可是有不少策略,都是朝着这个目前去努力。

Java 街恶梦:

当 programmer one 在读这篇文章的时候,他睡着了,而且作了个恶梦(每一个人都有作恶梦的时候)。

programmer one:nihahha,我要把你弄失效!(疯狂的状态)

缓存对象:别别,让我活着,他们还须要我,我还有孩子。

programmer one:每一个缓存对象在失效以前都会那样说。你从何时开始有孩子的?不用担忧,如今就永远消失吧!

哈哈哈哈哈……programmer one 恐怖的笑着,可是警笛打破了沉静,警察把 programmer one 抓了起来,而且控告他杀死了(失效)一个仍需被使用的缓存对象,他被押到了监狱。

programmer one 忽然醒了,他被吓到了,浑身是汗,他开始环顾四周,发现这确实是个梦,而后赶忙继续阅读这篇文章,努力的消除本身的恐慌。

在programmer one 醒来以后,他又开始阅读文章了。

 

7缓存算法

没有人能说清哪一种缓存算法优于其余的缓存算法

 

Least Frequently Used(LFU):

你们好,我是 LFU,我会计算为每一个缓存对象计算他们被使用的频率。我会把最不经常使用的缓存对象踢走。

 

Least Recently User(LRU):

我是 LRU 缓存算法,我把最近最少使用的缓存对象给踢走。

我老是须要去了解在何时,用了哪一个缓存对象。若是有人想要了解我为何总能把最近最少使用的对象踢掉,是很是困难的。

浏览器就是使用了我(LRU)做为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,我会把底部的对象踢走,而技巧就是:我会把最新被访问的缓存对象,放到缓存池的顶部。

因此,常常被读取的缓存对象就会一直呆在缓存池中。有两种方法能够实现我,array 或者是 linked list。

个人速度很快,我也能够被数据访问模式适配。我有一个你们庭,他们均可以完善我,甚至作的比我更好(我确实有时会嫉妒,可是不要紧)。我家庭的一些成员包括 LRU2 和 2Q,他们就是为了完善 LRU 而存在的。

 

Least Recently Used 2(LRU2):

我是 Least Recently Used 2,有人叫我最近最少使用 twice,我更喜欢这个叫法。我会把被两次访问过的对象放入缓存池,当缓存池满了以后,我会把有两次最少使用的缓存对象踢走。由于须要跟踪对象2次,访问负载就会随着缓存池的增长而增长。若是把我用在大容量的缓存池中,就会有问题。另外,我还须要跟踪那么不在缓存的对象,由于他们尚未被第二次读取。我比LRU好,并且是 adoptive to access 模式 。

 

Two Queues(2Q):

我是 Two Queues;我把被访问的数据放到 LRU 的缓存中,若是这个对象再一次被访问,我就把他转移到第二个、更大的 LRU 缓存。

我踢走缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。当缓存的访问负载是固定的时候,把 LRU 换成 LRU2,就比增长缓存的容量更好。这种机制使得我比 LRU2 更好,我也是 LRU 家族中的一员,并且是 adoptive to access 模式 。

 

Adaptive Replacement Cache(ARC):

我是 ARC,有人说我是介于 LRU 和 LFU 之间,为了提升效果,我是由2个 LRU 组成,第一个,也就是 L1,包含的条目是最近只被使用过一次的,而第二个 LRU,也就是 L2,包含的是最近被使用过两次的条目。所以, L1 放的是新的对象,而 L2 放的是经常使用的对象。因此,别人才会认为我是介于 LRU 和 LFU 之间的,不过不要紧,我不介意。

我被认为是性能最好的缓存算法之一,可以自调,而且是低负载的。我也保存着历史对象,这样,我就能够记住那些被移除的对象,同时,也让我能够看到被移除的对象是否能够留下,取而代之的是踢走别的对象。个人记忆力不好,可是我很快,适用性也强。

 

Most Recently Used(MRU):

我是 MRU,和 LRU 是对应的。我会移除最近最多被使用的对象,你必定会问我为何。好吧,让我告诉你,当一次访问过来的时候,有些事情是没法预测的,而且在缓存系统中找出最少最近使用的对象是一项时间复杂度很是高的运算,这就是为何我是最好的选择。

我是数据库内存缓存中是多么的常见!每当一次缓存记录的使用,我会把它放到栈的顶端。当栈满了的时候,你猜怎么着?我会把栈顶的对象给换成新进来的对象!

 

First in First out(FIFO):

我是先进先出,我是一个低负载的算法,而且对缓存对象的管理要求不高。我经过一个队列去跟踪全部的缓存对象,最近最经常使用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,而后把新的缓存对象加进去。我很快,可是我并不适用。

 

Second Chance:

你们好,我是 second chance,我是经过 FIFO 修改而来的,被你们叫作 second chance 缓存算法,我比 FIFO 好的地方是我改善了 FIFO 的成本。我是 FIFO 同样也是在观察队列的前端,可是很FIFO的马上踢出不一样,我会检查即将要被踢出的对象有没有以前被使用过的标志(1一个 bit 表示),没有没有被使用过,我就把他踢出;不然,我会把这个标志位清除,而后把这个缓存对象当作新增缓存对象加入队列。你能够想象就这就像一个环队列。当我再一次在队头碰到这个对象时,因为他已经没有这个标志位了,因此我马上就把他踢开了。我在速度上比 FIFO 快。

 

CLock:

我是 Clock,一个更好的 FIFO,也比 second chance 更好。由于我不会像 second chance 那样把有标志的缓存对象放到队列的尾部,可是也能够达到 second chance 的效果。

我持有一个装有缓存对象的环形列表,头指针指向列表中最老的缓存对象。当缓存 miss 发生而且没有新的缓存空间时,我会问问指针指向的缓存对象的标志位去决定我应该怎么作。若是标志是0,我会直接用新的缓存对象替代这个缓存对象;若是标志位是1,我会把头指针递增,而后重复这个过程,知道新的缓存对象可以被放入。我比 second chance 更快。

 

Simple time-based:

我是 simple time-based 缓存算法,我经过绝对的时间周期去失效那些缓存对象。对于新增的对象,我会保存特定的时间。我很快,可是我并不适用。

 

Extended time-based expiration:

我是 extended time-based expiration 缓存算法,我是经过相对时间去失效缓存对象的;对于新增的缓存对象,我会保存特定的时间,好比是每5分钟,天天的12点。

 

Sliding time-based expiration:

我是 sliding time-based expiration,与前面不一样的是,被我管理的缓存对象的生命起点是在这个缓存的最后被访问时间算起的。我很快,可是我也不太适用。

其余的缓存算法还考虑到了下面几点:

成本:若是缓存对象有不一样的成本,应该把那些难以得到的对象保存下来。

容量:若是缓存对象有不一样的大小,应该把那些大的缓存对象清除,这样就可让更多的小缓存对象进来了。

时间:一些缓存还保存着缓存的过时时间。电脑会失效他们,由于他们已通过期了。

根据缓存对象的大小而无论其余的缓存算法多是有必要的。

  

8电子邮件!

在读完这篇文章以后,programmer one 想了一下子,而后决定给做者发封邮件,他感受做者的名字在哪听过,可是已经想不起来了。无论怎样,他仍是把邮件发送出来了,他询问了做者在分布式环境中,缓存是怎么样工做的。

文章的做者收到了邮件,具备讽刺意味的是,这个做者就是面试 programmer one 的人  ,做者回复了……

在这一部分中,咱们来看看如何实现这些著名的缓存算法。如下的代码只是示例用的,若是你想本身实现缓存算法,可能本身还得加上一些额外的工做。

 

9LeftOver机制

在 programmer one 阅读了文章以后,他接着看了文章的评论,其中有一篇评论提到了 leftover 机制——random cache。

 

10Random Cache

我是随机缓存,我随意的替换缓存实体,没人敢抱怨。你能够说那个被替换的实体很倒霉。经过这些行为,我随意的去处缓存实体。我比 FIFO 机制好,在某些状况下,我甚至比 LRU 好,可是,一般LRU都会比我好。

 

11如今是评论时间

当 programmer one 继续阅读评论的时候,他发现有个评论很是有趣,这个评论实现了一些缓存算法,应该说这个评论作了一个链向评论者网站的连接,programmer one顺着连接到了那个网站,接着阅读。

 

看看缓存元素(缓存实体)

public class CacheElement

{

private Object objectValue;

private Object objectKey;

private int index;

private int hitCount; // getters and setters

}

这个缓存实体拥有缓存的key和value,这个实体的数据结构会被如下全部缓存算法用到。

 

12缓存算法的公用代码

public final synchronized void addElement(Object key, Object value)

{

int index;

Object obj;

// get the entry from the table

obj = table.get(key);

// If we have the entry already in our table

// then get it and replace only its value.

obj = table.get(key);

 

if (obj != null)

{

  CacheElement element;

  element = (CacheElement) obj;

  element.setObjectValue(value);

  element.setObjectKey(key);

  return;

}

上面的代码会被全部的缓存算法实现用到。这段代码是用来检查缓存元素是否在缓存中了,若是是,咱们就替换它,可是若是咱们找不到这个 key 对应的缓存,咱们会怎么作呢?那咱们就来深刻的看看会发生什么吧!

 

13现场访问

今天的专题很特殊,由于咱们有特殊的客人,事实上他们是咱们想要听的与会者,可是首先,先介绍一下咱们的客人:Random Cache,FIFO Cache。让咱们从 Random Cache开始。

 

看看随机缓存的实现

public final synchronized void addElement(Object key, Object value)

{

int index;

Object obj;

obj = table.get(key);

if (obj != null)

{

CacheElement element;// Just replace the value.

element = (CacheElement) obj;

element.setObjectValue(value);

element.setObjectKey(key);

return;

}// If we haven't filled the cache yet, put it at the end.

if (!isFull())

{

index = numEntries;

++numEntries;

}

else { // Otherwise, replace a random entry.

index = (int) (cache.length * random.nextFloat());

table.remove(cache[index].getObjectKey());

}

cache[index].setObjectValue(value);

cache[index].setObjectKey(key);

table.put(key, cache[index]);

}

 

看看FIFO缓算法的实现

public final synchronized void addElement(Objectkey, Object value)

{

int index;

Object obj;

obj = table.get(key);

if (obj != null)

{

CacheElement element; // Just replace the value.

element = (CacheElement) obj;

element.setObjectValue(value);

element.setObjectKey(key);

return;

}

// If we haven't filled the cache yet, put it at the end.

if (!isFull())

{

index = numEntries;

++numEntries;

}

else { // Otherwise, replace the current pointer,

// entry with the new one.

index = current;

// in order to make Circular FIFO

if (++current >= cache.length)

current = 0;

table.remove(cache[index].getObjectKey());

}

cache[index].setObjectValue(value);

cache[index].setObjectKey(key);

table.put(key, cache[index]);

}

 

看看LFU缓存算法的实现

public synchronized Object getElement(Object key)

{

Object obj;

obj = table.get(key);

if (obj != null)

{

CacheElement element = (CacheElement) obj;

element.setHitCount(element.getHitCount() + 1);

return element.getObjectValue();

}

return null;

}

public final synchronized void addElement(Object key, Object value)

{

Object obj;

obj = table.get(key);

if (obj != null)

{

CacheElement element; // Just replace the value.

element = (CacheElement) obj;

element.setObjectValue(value);

element.setObjectKey(key);

return;

}

if (!isFull())

{

index = numEntries;

++numEntries;

}

else

{

CacheElement element = removeLfuElement();

index = element.getIndex();

table.remove(element.getObjectKey());

}

cache[index].setObjectValue(value);

cache[index].setObjectKey(key);

cache[index].setIndex(index);

table.put(key, cache[index]);

}

public CacheElement removeLfuElement()

{

CacheElement[] elements = getElementsFromTable();

CacheElement leastElement = leastHit(elements);

return leastElement;

}

public static CacheElement leastHit(CacheElement[] elements)

{

CacheElement lowestElement = null;

for (int i = 0; i < elements.length; i++)

{

CacheElement element = elements[i];

if (lowestElement == null)

{

lowestElement = element;

}

else {

if (element.getHitCount() < lowestElement.getHitCount())

{

lowestElement = element;

}

}

}

return lowestElement;

}

 

今天的专题很特殊,由于咱们有特殊的客人,事实上他们是咱们想要听的与会者,可是首先,先介绍一下咱们的客人:Random Cache, FIFO Cache。让咱们从 Random Cache开始。

最重点的代码,就应该是 leastHit 这个方法,这段代码就是把

hitCount 最低的元素找出来,而后删除,给新进的缓存元素留位置。

 

看看LRU缓存算法实现

private void moveToFront(int index)

{

int nextIndex, prevIndex;

if(head != index)

{

nextIndex = next[index];

prevIndex = prev[index];

// Only the head has a prev entry that is an invalid index

// so we don't check.

next[prevIndex] = nextIndex;

// Make sure index is valid. If it isn't, we're at the tail

// and don't set prev[next].

if(nextIndex >= 0)

prev[nextIndex] = prevIndex;

else

tail = prevIndex;

prev[index] = -1;

next[index] = head;

prev[head] = index;

head = index;

}

}

public final synchronized void addElement(Object key, Object value)

{

int index;Object obj;

obj = table.get(key);

if(obj != null)

{

CacheElement entry;

// Just replace the value, but move it to the front.

entry = (CacheElement)obj;

entry.setObjectValue(value);

entry.setObjectKey(key);

moveToFront(entry.getIndex());

return;

}

// If we haven't filled the cache yet, place in next available

// spot and move to front.

if(!isFull())

{

if(_numEntries > 0)

{

prev[_numEntries] = tail;

next[_numEntries] = -1;

moveToFront(numEntries);

}

++numEntries;

}

else { // We replace the tail of the list.

table.remove(cache[tail].getObjectKey());

moveToFront(tail);

}

cache[head].setObjectValue(value);

cache[head].setObjectKey(key);

table.put(key, cache[head]);

}

这段代码的逻辑如 LRU算法 的描述同样,把再次用到的缓存提取到最前面,而每次删除的都是最后面的元素。

 

14结论

咱们已经看到 LFU缓存算法 和 LRU缓存算法的实现方式,至于如何实现,采用数组仍是 LinkedHashMap,都由你决定,不够我通常是小的缓存容量用数组,大的用 LinkedHashMap。

译者:lixiang

译文:http://www.zavakid.com/25

原文:http://www.jtraining.com/component/content/article/35-jtraining-blog/98.html

相关文章
相关标签/搜索