概述
我在《性能调优思考》一文中粗略的谈到了缓存。这里再另开一文并结合我自己写的一个例子来谈谈缓存的写法以及淘汰策略。缓存的用途场景会比较多,这里可以结合计算机硬件分层的思想从寄存器-->Cache(L1,L2,L3)-->主存-->磁盘就可以知道缓存的用途。一般web应用最基本的场景是就是存数据和取数据,比如存储介质是数据库,数据库不完全都是磁盘IO,数据库本身也会在内存中开辟一块空间用来存一些热点数据。总归一句话:内存缓存就是为了利用内存本身的特性来加快访问数据的速度。然而内存有一个非常致命的地方就是容量小,不能把所有的东西都放到内存中。这样就有了缓存策略一说,归根结底是为了提高内存的利用率(命中率)。围绕内存有限的这么一个基本事实,要做的如果提高内存利用率,一般内存缓存都会有淘汰策略。
一般有两种淘汰策略:
一.访问末位淘汰
这个策略是基于这么一个假设:越是被访问的数据我们有理由相信它下次被访问的概率更大。
二.时间淘汰
时间淘汰就是在固定的时间内对数据进行清理。这也是为了提高内存利用率,因为末位淘汰策略是只有新的数据进来的时候才会淘汰掉老的数据,而有时候内存中其实有大部分的数据都是无用数据,适时的清理也是可以提高整个内存的利用率。
这里再讲两种应用场景,然后再分析各应用场景下所对应的策略。
一.数据平均场景
这种场景说明整个内存的访问不会非常集中在某一块,像这种场景下缓存的命中率会比较低,其实不太好发挥内存缓存的作用。这个更好理解,假如总共100万数据,缓存只能容纳1万数据,这样如果100万数据都会被平均访问的话每次数据被命中的概率只有1/100。这种场景用时间淘汰策略可能会更好一些。因为末位淘汰的假设在这里已经不成立了。
二.热点数据场景
这样的场景其实非常普遍,而这种场景也是最能发挥内存缓存的效能。一般这种场景只要淘汰策略好的话,可以很好的提高整体应用的性能。
总结一下,这里两种淘汰策略基本上会进行混合搭配使用,好了,这里结合我具体写的一个例子来讨论。
这里先简单描述我的整体思想:
第一:用普通的HashMap作为容器
第二:用时间淘汰策略,并支持热点数据前置。
第三:"分桶"的思想,前面两点都好理解,估计这个会有些人提出疑问,我这里详细说一下
a).所谓分桶的思想其实就是一个大容器中会切分成多个小容器。然后用运用一定的策略进行分发到不同的"桶"中,其实这里应用了分布式缓存的一些特性,其核心思想旨在让每个小桶能够独立运作,互不干扰。下面讲代码的时候我会具体说到为什么要进行分桶,到底在解决什么问题,还会讲分桶的一些弊端
b).桶的分配策略我用了最简单的取模运算。
开始上代码了,
/**
* @author sanxing 2012-11-3 下午10:33:30 本缓存旨在解决数据在内存区块缓存策略
*
* <p>
* 利用HashMap作为最基本容器进行存储
* <p>
* <p>
* 采用时间淘汰策略,并支持热点数据前置(更新其时间)。
* </p>
* <p>
* "分桶"的思想
* <i>
* 所谓分桶的思想其实就是一个大容器中会切分成多个小容器。然后用运用一定的策略进行分发到不同的
* "桶"中,其实这里应用了分布式缓存的一些特性,其核心思想旨在让每个小桶能够独立运作
* ,互不干扰。这样当桶清理(@see remove())的时候会保证达到影响最小。
* </i>
* </p>
*
* <p>
* 本缓存策略是建立在数据均匀分配的一个假设上,也就是说海量的数据会平均分配到各个桶上,这样才能保证每个桶的利用率达到最高,性能最好。
* 如果出现集中式缓存也就是数据集约到其中几个桶上,而其他桶的拿不到数据,不能使用此缓存或者需要改变数据的分发策略。
* </p>
*
*/
public class LRUCache {
/**
* 缓存过期时间
*/
private long aliveTime;
/**
* 桶的最大容量
*/
private int maxSize;
/**
* 每个小桶的容量
*/
private int everyPoolSize;
/**
* 命中次数
*/
private int hit;
/**
* 丢失次数
*/
private int missHit;
private Map<Integer, Map<String, CacheObject>> cacheMap;
/**
* 小桶的个数
*/
private int mod = 32;
public int getHit() {
return hit;
}
public int getMissHit() {
return missHit;
}
public void setMod(int mod) {
this.mod = mod;
}
public int getMod() {
return mod;
}
public int getEveryPoolSize() {
return everyPoolSize;
}
public LRUCache(int maxSize, long aliveTime) {
this.maxSize = maxSize;
this.aliveTime = aliveTime;
cacheMap = new HashMap<Integer, Map<String, CacheObject>>();
for (int i = 0; i < mod; i++) {
Map<String, CacheObject> sonMap = new HashMap<String, CacheObject>();
cacheMap.put(i, sonMap);
}
/**
* 这里并不是准确的,因为当不是mod的倍数的时候会多出<mod的个数,举一个简单的例子:假如maxSize=33,mod=32,那么
* 每个小桶的poosize=2,这样加起来是64,所以并不精准。 <br />
* 如果需要准确的可以寻求其他策略
*/
this.everyPoolSize = (this.maxSize + mod - 1) / mod;
}
public void put(String key, Object val) {
// 如果小于最大数量
int keyMod = getKeyMod(key);
Map<String, CacheObject> sonMap = null;
synchronized (sonMap = cacheMap.get(keyMod)) {
if (sonMap.size() >= everyPoolSize) {
/**
* 如果发现桶已经满了时候,触发remove对整个桶进行清理,有木有很像JVM中的FGC啊
* 其实基本思想就是这样,但是由于HashMap并非线程安全的,所以必须对桶进行加锁
* 而本身清理是有一定耗时的,这样势必降低缓存的访问。所以必须降低remove事件的
* 发生,也就像JVM需要减少FGC一样。这里会有许多策略。
* 而分桶的作用就出现了,这样触发remove事件,只需要锁住小桶,而其他小桶并不会
* 因此受到影响仍然可以访问。这里也是遵循锁运用基本原则:只在需要的地方加锁。
*/
remove(keyMod);
}
if (sonMap.size() < everyPoolSize) {
sonMap.put(key,
new CacheObject(val, System.currentTimeMillis()));
}
}
}
/**
* 最简单的取模数,进行桶的分发策略
*
* @param key
* @return
*/
private int getKeyMod(String key) {
return (key.hashCode() & Integer.MAX_VALUE) % mod;
}
/**
* 负责对桶进行清理工作,把所有已经过期的数据全部移除出缓存区,这项工作还是比较耗时的。
*
* @param keyMod
*/
public void remove(int keyMod) {
synchronized (cacheMap.get(keyMod)) {
Map<String, CacheObject> oldMap = cacheMap.get(keyMod);
Map<String, CacheObject> newMap = new HashMap<String, CacheObject>();
for (Map.Entry<String, CacheObject> entry : oldMap.entrySet()) {
if ((System.currentTimeMillis() - entry.getValue().getTime()) < aliveTime) {
newMap.put(entry.getKey(), entry.getValue());
}
}
oldMap = null;
cacheMap.put(keyMod, newMap);
}
}
/**
* 拿到缓存区的数据,这里的策略会先判断数据是否过期,过期则移除
*
* @param key
* @return
*/
public Object get(String key) {
int keyMod = getKeyMod(key);
Map<String, CacheObject> map = null;
synchronized (map = cacheMap.get(keyMod)) {
CacheObject co = map.get(key);
if (co != null) {
if ((System.currentTimeMillis() - co.getTime()) > aliveTime) {
// 过期则移除,这里类似于懒加载,和JVM里的YGC有点类似
map.remove(key);
missHit++;
} else {
// 没有过期
hit++;
/**
* 热点数据策略,当数据被访问时更新它的时间以延长它在内存中驻留的时间。 保证下次能够顺利命中。
*/
co.setTime(System.currentTimeMillis());
return co.getObj();
}
}
}
return null;
}
/**
* 获取整个数据区的数据容量
*
* @return
*/
public int size() {
int allSize = 0;
// 这里不加锁,不保证得到的size一定准确
for (int i = 0; i < mod; i++) {
allSize += cacheMap.get(i).size();
}
return allSize;
}
/**
* 拿到每个桶的容量。
*
* @param mod
* @return
*/
public int poolSize(int mod) {
if (mod < 0 || mod > this.mod) {
throw new IllegalArgumentException("mod 参数不在桶的区域内,mod=" + mod);
}
return cacheMap.get(mod).size();
}
/**
* 缓存数据结构
*
* @author sanxing 2012-11-4 下午01:48:47
*
*/
private class CacheObject {
private Object obj;
private long time;
public CacheObject(Object obj, long time) {
this.obj = obj;
this.time = time;
}
public Object getObj() {
return obj;
}
public void setTime(long time) {
this.time = time;
}
public long getTime() {
return time;
}
}
}
上面就是整个代码以及一些注释,最难的是其实是平均的把数据分发到每个桶上,也就是分发策略,保证缓存的最大利用率,这个需要实际测试来进行测试。我在这里作了最简单的取模运算。从统计学来讲,当数据是海量无序的时候应该基本符合平均分配的原则。
整个策略及思想就完了。经过一段时间的思考,发现整个计算机系统乃致其他非计算机场景都会遵循类似的策略。这似乎也是思考的魅力所在,能把整个计算机整合起来思考并进行发散是非常有趣的,有时候甚至有茅塞顿开的感觉。
最后
以上就是丰富棉花糖为你收集整理的内存缓存策略探析的全部内容,希望文章能够帮你解决内存缓存策略探析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复