前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何动手撸一个简单的LFU缓存

如何动手撸一个简单的LFU缓存

作者头像
我是攻城师
发布2019-07-30 15:58:47
1.1K0
发布2019-07-30 15:58:47
举报
文章被收录于专栏:我是攻城师我是攻城师

今天我们来看下,如何用代码来实现一个简单的LFU缓存。

我们知道缓存置换算法主流的有三种,分别是:

(1) FIFO:First In First Out,先进先出策略

(2) LFU:Least Frequently Used,最不经常使用策略

(3) LRU:Least Recently Used,最近最少使用策略

关于第一种FIFO策略的实现,比较简单,可采用固定长度的数组和链表来处理,这里就不重点说了。今天我们的重点是LFU缓存的实现。

LFU 全称 Least Frequently Used,从名字上我们就能看出来这个算法是基于数据访问频率(次数)来淘汰数据的,也就是说系统会记录一段时间内所有数据的访问次数,当缓存区满的时候,会优先淘汰访问次数最少的数据。其核心思想:如果一个数据在最近一段时间内访问次数很少,则在将来一段时间内被访问的可能性也很小。显然,这是一种合理的算法,因为到目前为止最少使用的页面,很可能也是将来最少访问的页面。缓存的每个数据都有引用计数,所有数据按照引用计数排序,具有相同引用计数的数据按照时间排序。

我们来看下LFU缓存算法的实现代码:

代码语言:javascript
复制
package algorithm.cache_algorithm;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/****
 * 实现一个 LFU 缓存
 * Least Frequently Used,最不经常使用策略
 */
public class LFUCache {

    private   int capacity;//缓存队列的容量值

    private Map<Integer,AccessRate> cache;//保存缓存数据

    public LFUCache(int capacity) {
        this.capacity = capacity;
        cache=new HashMap<>();
    }

    public void put(int key,int value){
        AccessRate v=cache.get(key);
        if(v==null){//第一次插入

            if(cache.size()==capacity){//容量已超
                cache.remove(getEvictKey());//驱逐被淘汰的数据
            }
            //新增
            v=new AccessRate(key,value,1,System.nanoTime());
            cache.put(key,v);
            log("add",v);
        }else{
            //更新状态
            v.value=value;
            v.hitCount=v.hitCount+1;
            v.lastTime=System.nanoTime();
            cache.put(key,v);
            log("update",v);
        }


    }



    public int get(int key){
        AccessRate v=cache.get(key);
        if(v!=null){
            v.hitCount=v.hitCount+1;
            v.lastTime=System.nanoTime();
            log("query",v);
            return v.value;
        }
        log("query",new AccessRate(key));
        return -1;
    }

    public void remove(int key){
        AccessRate v=cache.remove(key);
        if(v!=null){
            log("remove",v);
        }else{
            log("remove",new AccessRate(key));
        }

    }


    //记录操作明细,方便理解
    public void log(String operator,AccessRate accessRate){
        if(accessRate.isNotNull()){
            System.out.println(operator+" => "+accessRate.toString()+" "+deatail());
        }else {
            System.out.println(operator+" => the key "+accessRate.key+" not exist"+" "+deatail());
        }


    }

    public String deatail(){
        StringBuilder sb=new StringBuilder();
        sb.append("当前缓存中有效的key=");
        for (AccessRate accessRate:cache.values()){
            sb.append(accessRate.key+",");
        }
        return sb.deleteCharAt(sb.length()-1).toString();
    }

    //获取缓存里面,需要被淘汰的数据,复杂度O(N),可用优先级队列(堆)进行优化
    public Integer getEvictKey(){
        AccessRate min= Collections.min(cache.values());
        log("evict",min);
        return min.key;
    }




    class AccessRate implements Comparable<AccessRate> {

        private   Integer key;//访问的数据key
        private   Integer value;//访问数据的value
        private   Integer hitCount;//记录访问次数
        private   Long lastTime;//最新的访问时间



        public AccessRate(Integer key, Integer value, Integer hitCount, Long lastTime) {
            this.key = key;
            this.value = value;
            this.hitCount = hitCount;
            this.lastTime = lastTime;
        }

        public AccessRate() {
        }

        public AccessRate(Integer key) {
            this.key=key;
        }

        public boolean isNotNull(){
            return key!=null&&value!=null;
        }

        @Override
        public int compareTo(AccessRate o) {
            int firstSort=hitCount.compareTo(o.hitCount);
            if(firstSort!=0){////如果命中次数不相等,使用命中次数排序
                return firstSort;
            }else {//如果命中次数相等,就使用访问的时间进行排序
                return lastTime.compareTo(o.lastTime);
            }
        }

        @Override
        public String toString() {
            return "AccessRate{" +
                    "key=" + key +
                    ", value=" + value +
                    ", hitCount=" + hitCount +
                    ", lastTime=" + lastTime +
                    '}';
        }
    }

}

(友情提示:代码块可左右滑动)

代码并不复杂,为了方便大家观察到细节,我特意在代码里面加了相关log输出,下面我们来测试这个缓存算法:

代码语言:javascript
复制
public static void main(String[] args) {
        //构建一个容量为3的LFU缓存实例
        LFUCache cache=new LFUCache(3);

        cache.put(1,1);// 添加 1
        cache.put(2,2);// 添加 2
        cache.get(1);// 访问 1
        cache.get(2);// 访问 2
        cache.put(3,3);// 添加 3
        cache.put(4,4);// 缓存满,先淘汰访问次数少的3,然后再添加 4
        cache.get(5);// 访问 5
        cache.remove(4); // 移除 4
        cache.remove(4);// 移除 4

    }

完整代码,请访问我的github:https://github.com/qindongliang/Java-Note/blob/master/src/main/java/algorithm/cache_algorithm/LFUCache.java

运行上面的代码,我们的控制台输出信息如下:

代码语言:javascript
复制
add => AccessRate{key=1, value=1, hitCount=1, lastTime=113379228623808} 当前缓存中有效的key=1
add => AccessRate{key=2, value=2, hitCount=1, lastTime=113379229329163} 当前缓存中有效的key=1,2
query => AccessRate{key=1, value=1, hitCount=2, lastTime=113379229389607} 当前缓存中有效的key=1,2
query => AccessRate{key=2, value=2, hitCount=2, lastTime=113379229445363} 当前缓存中有效的key=1,2
add => AccessRate{key=3, value=3, hitCount=1, lastTime=113379229493594} 当前缓存中有效的key=1,2,3
evict => AccessRate{key=3, value=3, hitCount=1, lastTime=113379229493594} 当前缓存中有效的key=1,2,3
add => AccessRate{key=4, value=4, hitCount=1, lastTime=113379229660888} 当前缓存中有效的key=1,2,4
query => the key 5 not exist 当前缓存中有效的key=1,2,4
remove => AccessRate{key=4, value=4, hitCount=1, lastTime=113379229660888} 当前缓存中有效的key=1,2
remove => the key 4 not exist 当前缓存中有效的key=1,2

可以看到结果是没问题的,在上面的测试中,我们设置缓存的容量大小为3,然后先添加了两条数据1,2,然后接着分别对1和2进行了查询,注意这个时候1和2的引用计数会增加2,并且他们的时间也会更新,接着我们添加了3和4,注意在添加4的时候由于缓存容量已经满了,为了能让4添加进来,我们必须根据淘汰一条数据,那么这个淘汰数据应该是谁呢?毫无疑问就是3了,因为3的访问次数最少。注意如果这里3的次数也是2,那么算法会根据时间选择一条时间最早的数据,这个时候淘汰的数据就是1了,最后我们访问了一条不存在的数据5,并且对同一个key=4的数据,删除了2次,可以看到结果也是没有问题的。

该算法的时间复杂度最坏的情况为O(N),原因在于在淘汰数据的时候,我们偷了个懒,用的是Collections.min方法,这个函数内部会迭代整个values里面的数据来找到需要被淘汰的数据,当数据量大的时候性能不会太好,所以在这个地方,我们可以使用堆这种数据结构来优化性能,从而让时间复杂度降至O(logN),如果是在Java里面,我们可以直接借助优先级队列(底层结构堆)来实现,并提供相关的自定义排序策略。

本文主要介绍了LFU缓存算法的简单实现和复杂度分析,LFU算法可以避免偶发性的、周期性的批量操作会导致LRU算法命中率急剧下降,缓存污染情况比较严重的问题。但其缺点也很明显,面对一段时间内热点数据,其效果没有LRU好,LFU在存在大量的历史数据的高频访问时,如果此时新来了很多访问频次略低于历史数据的时候,新的热点数据由于频次略低,在容量有限的时候很有可能就被淘汰了,从而造成缓存miss,此外从实现的复杂度上来分析,LFU 需要维护一个队列记录所有数据的访问记录,每个数据都要维护引用计数,内存消耗和性能消耗都较高。LFU整体上在空间和时间复杂度上均高于LRU算法,这也是为什么LRU算法更受欢迎的原因,在下篇文章我们会重点介绍下如何实现一个LRU缓存。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档