缓存学习-[转]缓存淘汰算法--LFU算法

 程老师原文:http://flychao88.iteye.com/blog/1977642
原文如下:

1. LFU类

1.1. LFU

1.1.1. 原理

LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

1.1.2. 实现

LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。

具体实现如下:
缓存学习-[转]缓存淘汰算法--LFU算法_第1张图片

 

1. 新加入数据插入到队列尾部(因为引用计数为1);

2. 队列中的数据被访问后,引用计数增加,队列重新排序;

3. 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

1.1.3. 分析

l 命中率

一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。

l 复杂度

需要维护一个队列记录所有数据的访问记录,每个数据都需要维护引用计数。

l 代价

需要记录所有数据的访问记录,内存消耗较高;需要基于引用计数排序,性能消耗较高。

 

1.2. LFU*

1.2.1. 原理

基于LFU的改进算法,其核心思想是“只淘汰访问过一次的数据”。

1.2.2. 实现

LFU*数据缓存实现和LFU一样,不同的地方在于淘汰数据时,LFU*只淘汰引用计数为1的数据,且如果所有引用计数为1的数据大小之和都没有新加入的数据那么大,则不淘汰数据,新的数据也不缓存

1.2.3. 分析

l 命中率

和LFU类似,但由于其不淘汰引用计数大于1的数据,则一旦访问模式改变,LFU*无法缓存新的数据,因此这个算法的应用场景比较有限。

l 复杂度

需要维护一个队列,记录引用计数为1的数据。

l 代价

相比LFU要低很多,不需要维护所有数据的历史访问记录,只需要维护引用次数为1的数据,也不需要排序。

1.3. LFU-Aging

1.3.1. 原理

基于LFU的改进算法,其核心思想是“除了访问次数外,还要考虑访问时间”。这样做的主要原因是解决LFU缓存污染的问题。

1.3.2. 实现

虽然LFU-Aging考虑时间因素,但其算法并不直接记录数据的访问时间,而是通过平均引用计数来标识时间。

LFU-Aging在LFU的基础上,增加了一个最大平均引用计数。当当前缓存中的数据“引用计数平均值”达到或者超过“最大平均引用计数”时,则将所有数据的引用计数都减少。减少的方法有多种,可以直接减为原来的一半,也可以减去固定的值等。

1.3.3. 分析

l 命中率

LFU-Aging的效率和LFU类似,当访问模式改变时,LFU-Aging能够更快的适用新的数据访问模式,效率要高。

l 复杂度

在LFU的基础上增加平均引用次数判断和处理。

l 代价

和LFU类似,当平均引用次数超过指定阈值(Aging)后,需要遍历访问列表。

 

1.4. LFU*-Aging

1.4.1. 原理

LFU*和LFU-Aging的合成体。

1.4.2. 实现

略。

1.4.3. 分析

l 命中率

和LFU-Aging类似。

l 复杂度

比LFU-Aging简单一些,不需要基于引用计数排序。

l 代价

比LFU-Aging少一些,不需要基于引用计数排序。

1.5. Window-LFU

1.5.1. 原理

Windows-LFU是LFU的一个改进版,差别在于Window-LFU并不记录所有数据的访问历史,而只是记录过去一段时间内的访问历史,这就是Window的由来,基于这个原因,传统的LFU又被称为“Perfect-LFU”。

1.5.2. 实现

与LFU的实现基本相同,差别在于不需要记录所有数据的历史访问数据,而只记录过去一段时间内的访问历史。具体实现如下:
缓存学习-[转]缓存淘汰算法--LFU算法_第2张图片

 

1)记录了过去W个访问记录;

2)需要淘汰时,将W个访问记录按照LFU规则排序淘汰 

举例如下:

假设历史访问记录长度设为9,缓存大小为3,图中不同颜色代表针对不同数据块的访问,同一颜色代表针对同一数据的多次访问。
缓存学习-[转]缓存淘汰算法--LFU算法_第3张图片

样例1:黄色访问3次,蓝色和橘色都是两次,橘色更新,因此缓存黄色、橘色、蓝色三个数据块

样例2:绿色访问3次,蓝色两次,暗红两次,蓝色更新,因此缓存绿色、蓝色、暗红三个数据块

 

1.5.3. 分析

l 命中率

Window-LFU的命中率和LFU类似,但Window-LFU会根据数据的访问模式而变化,能够更快的适应新的数据访问模式,”缓存污染“问题不严重。

l 复杂度

需要维护一个队列,记录数据的访问流历史;需要排序。

l 代价

Window-LFU只记录一部分的访问历史记录,不需要记录所有的数据访问历史,因此内存消耗和排序消耗都比LFU要低。

1.6. LFU类算法对比

由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析。 

对比点

对比

命中率

Window-LFU/LFU-Aging > LFU*-Aging > LFU > LFU*

复杂度

LFU-Aging > LFU>  LFU*-Aging  >Window-LFU > LFU*

代价

LFU-Aging > LFU > Window-LFU > LFU*-Aging  > LFU*

**********************************原文结束,学习笔记开始***********************************************
没有用过这块,只看原理大概明白,网上找个例子
http://blog.csdn.net/definite_things/article/details/45705307
为了便于理解修改下,核心代码没变,只是增加输出字段,便于理解。
import java.util.*;

public class LFUAgingMap<K, V> extends HashMap<K, V> {
    private static final int DEFAULT_MAX_SIZE = 3;
    private int maxSize = DEFAULT_MAX_SIZE;
    Map<K, HitRate> km = new HashMap<K, HitRate>();

    public LFUAgingMap() {
        this(DEFAULT_MAX_SIZE);
    }

    public LFUAgingMap(int maxSize) {
        super(maxSize);
        this.maxSize = maxSize;
    }

    @Override
    public V get(Object key) {
        V v = super.get(key);
        if (v != null) {
            HitRate hitRate = km.get(key);
            hitRate.hitCount += 1;
            hitRate.atime = System.nanoTime();
        }
        return v;
    }
    @Override
    public V put(K key, V value) {
    //当要存入缓存大于预定范围时,要去掉最后一个。
        while (km.size() >= maxSize) {
            K k = getLFUAging();
            km.remove(k);
            this.remove(k);
        }
        V v = super.put(key, value);
        //新缓存纪录次数为1,时间为当前时间。
        km.put(key, new HitRate(key, 1, System.nanoTime()));
        return v;
    }
    private K getLFUAging() {
        HitRate min = Collections.min(km.values());
        return min.key;
    }
    class HitRate implements Comparable<HitRate> {
        public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public Integer getHitCount() {
return hitCount;
}
public void setHitCount(Integer hitCount) {
this.hitCount = hitCount;
}
public Long getAtime() {
return atime;
}
public void setAtime(Long atime) {
this.atime = atime;
}
K key;
        Integer hitCount; // 命中次数
        Long atime; // 上次命中时间

        public HitRate(K key, Integer hitCount, Long atime) {
            this.key = key;
            this.hitCount = hitCount;
            this.atime = atime;
        }
        @Override
        public int compareTo(HitRate o) {
        //先按照次数比较
            int hr = hitCount.compareTo(o.hitCount);
            //次数相同再按照访问时间比较
            return hr != 0 ? hr : atime.compareTo(o.atime);
        }
    }
    public static void main(String[] as) throws Exception {
        LFUAgingMap<String, String> cache = new LFUAgingMap<String, String>();
        cache.put("a", "a");
        cache.put("b", "b");
        cache.put("c", "c");
        //增加a的次数
        cache.get("a");
        cache.get("a");
        
        cache.put("d", "d");
        cache.get("d");
        cache.get("c");
        cache.put("e", "e");

        for (String key : cache.keySet()) {           
            System.out.println("key:"+key+";count:"+cache.km.get(key).getHitCount()+";time:"+cache.km.get(key).getAtime());
        }
    }
}
输出结果如下:
分析下结果,默认缓存大小为3:
        cache.put("a", "a");
        cache.put("b", "b");
        cache.put("c", "c");
此时缓存为abc,对应次数各为1
a:1
b:1
c:1
程序执行完
    cache.get("a");
        cache.get("a");
此时;变为:
a:3
b:1
c:1
  继续执行: cache.put("d", "d");
        cache.get("d");
  cache.get("c");
此时缓存超过默认大小,根据规则,要删除一条,b,c次数相同,但是b的时间访问在前,所以删除b,
对应缓存值变为:
a:3
c:2
d:2
继续执行:  cache.put("e", "e");
  此时有超出缓存默认大小,要删除一条,c,d次数相同,但是依据程序执行顺序d在前,c访问在后,所以删除d.,加入e.
对应缓存值变为:
a:3
c:2
e:1

你可能感兴趣的:(缓存学习-[转]缓存淘汰算法--LFU算法)