本文来自网络,经整理而成。
1、什么是缓存?
缓存是存贮数据(使用频繁的数据)的临时地方。因为取原始数据的代价太大了,所以利用缓存可以减轻数据库的负载,提升访问速度,加快用户的请求响应。
缓存可以认为是数据的池,这些数据是从数据库里的真实数据复制出来的,并且为了能被取回,被标上了标签(键 ID)。
缓存什么内容:大部分缓存算法使用预取策略来提前将部分磁盘数据放入缓存,以进一步减少磁盘I/O,加大缓存命中率。通过记录、分析以往的数据请求来预测将来可能被频繁请求到的数据段,将这个数据段放入缓存。
2、缓存的几个基本术语
①命中(命中率):
当客户发起一个请求(我们说他想要查看一个产品信息),我们的应用接受这个请求,并且如果是在第一次检查缓存的时候,缓存中没有这个数据,那么就需要去数据库读取产品信息,这个称为缓存命中。命中率,就是100次访问请求100条数据,在缓存中可以得到的有80条数据,那么这个时候缓存命中率就是80%。
需要注意如下两点:
如果还有缓存的空间,那么,没有命中的对象会被存储到缓存中来。如果缓存满了,而又没有命中缓存,那么就会按照某一种策略,把缓存中的旧对象剔除,而把新的对象加入缓存池。而这些策略统称为替代策略(也就是缓存算法),这些策略(缓存算法)会决定到底应该剔除哪些对象。
3、存储成本:
当没有命中时,我们会从数据库中取出数据,然后放入缓存。而把这个数据放入缓存所需要的时间和空间,就是存储成本。
4、失效:
当存在缓存中的数据需要更新时,就一位着缓存中的这个数据失效了。
5、替代策略(缓存算法)
当缓存没有命中时,并且缓存容量已经满了,就需要在缓存中剔除一个老的数据(根据剔除策略的不同指代不同的数据),加入一条新的数据条目。那么到底应该剔除什么数据条目,就由替代策略来决定,也就是由缓存算法来实现。
缓存策略的分类:
由于不同系统的数据访问模式不尽相同,同一种缓存策难以在各种数据访问模式下均取得满意性能,研究人员提出不同缓存策略以适应不同需求。
缓存策略可分为以下几类:
①基于访问时间:此类算法按各缓存项的被访问时间来组织缓存队列,决定替换对象。如LRU。
LRU (Least Recently Used)是一种应用广泛的缓存算法。该算法维护一个缓存项队列,队列中的缓存项按每项的最后被访问时间排序。
当缓存空间已满时,将处于队尾,即删除最后一次被访问时间距现在最久的项,将新的区段放入队列首部。但LRU算法仅维护了缓存块的访
问时间信息,没有考虑被访问频率等因素,在某些访问模式下无法获得理想命中率
②基于访问频率:此类算法用缓存项的被访问频率来组织缓存。如LFU、LRU-2、2Q。
LFU (Least Frequently Used)按每个缓存块的被访问频率将缓存中的各块排序,当缓存空间已满时,替换掉缓存队列中访问频率最低的一项。
与LRU的缺点类似, LFU仅维护各项的被访问频率信息,对于某缓存项,如果该项在过去有着极高的访问频率而最近访问频率较低,当缓存空间
已满时该项很难被从缓存中替换出来,进而导致命中率下降
LRU2会把被两次访问过的对象放入缓存池,当缓存池满了之后,我会把有两次最少使用的缓存对象踢走。因为需要跟踪对象2次,
访问负载就会随着缓存池的增加而增加。如果把我用在大容量的缓存池中,就会有问题。
2Q把被访问的数据放到 LRU 的缓存中,如果这个对象再一次被访问,把他转移到第二个、更大的 LRU 缓存。
③访问时间与频率兼顾:通过兼顾访问时间与频率,使得在数据访问模式变化时缓存策略仍有较好性能。如FBR、LRFU、ALRFU。
多数此类算法具有一个可调或自适应参数,通过该参数的调节使缓存策略在基于访问时间与频率间取得一定平衡。
FBR(Frequency Based Replacement)维护一个LRU队列,并将该队列分为New、Middle、Old三段。对队列中的每一缓存项均维护一
个计数值。当缓存中的一项被命中时,被命中的缓存项被移至New段的MRU端,如果该项原本位于Old或Middle段,则其计数值加1,原位于
New段则计数值不变。当进行替换操作时,删除Old段计数值最小的一项(LRU端)。
LRFU(Least Recently Frequently Used)为每一个缓存项维护一个权值C(x),其初始值为0, C(x)按以下公式变化。在t时刻, C(x)
=1+2-λC(x): x被访问到2-λC(x) : x未被访问进行替换操作时,C(x)值最小的一项被删除。当时, LRFU算法行为类似于LFU;而当时,
该算法行为逼近LRU算法。该算法通过选用合适的λ值来获得时间与频率因素的平衡。LRFU虽然通过一个值来兼顾访问时间与频率因素,
但由于值固定,当访问模式变化时,该算法无法做出相应的调整而造成性能下降。ALRFU(Adaptive LRFU)在此方面对LRFU进行了改进。
通过对数据访问模式的历史进行监控,ALRFU动态调整值来适应数据访问模式的改变,表现出比LRFU更好的适应性。
④基于访问模式:某些应用有较明确的的数据访问特点,进而产生与其相适应的缓存策略。
还有很多其他的缓存策略,这里仅再介绍First in First out(FIFO)缓存算法和Random Cache缓存算法。
First in First out(FIFO):先进先出,一个低负载的算法,并且对缓存对象的管理要求不高。通过一个队列去跟踪所有的缓存对象
,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓
存对象加进去。很快,但并不实用。
Random Cache:随机缓存,当维护缓存的队列满时,会随意的替换缓存实体。并不实用,但是在某些特定的环境下效果比较OK。
用到的缓存元素(缓存实体)
publicclass CacheElement { privateObject objectValue; privateObject objectKey; privateint index; privateint hitCount; // getters and setters }
这个缓存实体拥有缓存的key和value,这个实体的数据结构会被以下所有缓存算法用到。
缓存算法的公用代码
public final synchronized void addElement(Object key, Object value)
{
intindex;
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 对应的缓存,该怎么做呢?!
看看随机缓存的实现
public final synchronized void addElement(Object key, Object value) { intindex; 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) { intindex; 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]); }
public synchronized Object getElement(Object key) { Object obj; obj = table.get(key); if(obj != null) { CacheElement element = (CacheElement) obj; element.setHitCount(element.getHitCount() + 1); returnelement.getObjectValue(); } returnnull; } publicfinal 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]); } publicCacheElement removeLfuElement() { CacheElement[] elements = getElementsFromTable(); CacheElement leastElement = leastHit(elements); returnleastElement; } publicstatic CacheElement leastHit(CacheElement[] elements) { CacheElement lowestElement = null; for(inti = 0; i < elements.length; i++) { CacheElement element = elements[i]; if(lowestElement == null) { lowestElement = element; } else{ if(element.getHitCount() < lowestElement.getHitCount()) { lowestElement = element; } } } returnlowestElement; }
最重点的代码,就应该是 leastHit 这个方法,这段代码就是把hitCount 最低的元素找出来,然后删除,给新进的缓存元素留位置。
看看LRU缓存算法实现
private void moveToFront(intindex) { intnextIndex, 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; } } publicfinal synchronized void addElement(Object key, Object value) { intindex;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算法 的描述一样,把再次用到的缓存提取到最前面,而每次删除的都是最后面的元素。
至于如何实现LFU缓存算法 和 LRU缓存算法,采用数组还是 LinkedHashMap都不限定。不过一般是小的缓存容量用数组,大的用 LinkedHashMap。