FIFO/LRU/LFU三种缓存算法

更多精彩文章。java

《微服务不是所有,只是特定领域的子集》面试

《“分库分表" ?选型和流程要慎重,不然会失控》算法

这么多监控组件,总有一款适合你vim

《Linux生产环境上,最经常使用的一套“vim“技巧》后端

《使用Netty,咱们到底在开发些什么?》缓存

最有用系列:bash

《Linux生产环境上,最经常使用的一套“vim“技巧》数据结构

《Linux生产环境上,最经常使用的一套“Sed“技巧》并发

《Linux生产环境上,最经常使用的一套“AWK“技巧》微服务

欢迎Linux和java后端的同窗关注公众号。

JVM内缓存是缓存体系中重要的一环,最经常使用的有FIFO/LRU/LFU三种算法。

一、FIFO是简单的队列,先进先出。
二、LRU是最近最少使用,优先移除最久未使用的数据。是时间维度
三、LFU是最近最不经常使用,优先移除访问次数最少的数据。是统计维度

因为过时也是缓存的一个重要特色。全部在设计这三种缓存算法时,须要额外的存储空间去存储这个过时时间。

如下将讨论这三种缓存算法的操做和设计要点,暂未考虑高并发环境。

FIFO

先进先出,若是缓存容量满,则优先移出最先加入缓存的数据;其内部可使用队列实现。

特色

1)Object get(key):获取保存的数据,若是数据不存在或者已通过期,则返回null。
2)void put(key,value,expireTime):加入缓存,不管此key是否已存在,均做为新key处理(移除旧key);若是空间不足,则移除已过时的key,若是没有,则移除最先加入缓存的key。过时时间未指定,则表示永不自动过时。
3)此题须要注意,咱们容许key是有过时时间的,这一点与普通的FIFO有所区别,因此在设计此题时须要注意。(也是面试考察点,此题偏设计而非算法)

普通的FIFO或许你们都能很简单的写出,此处增长了过时时间的特性,因此在设计时须要多考虑。以下示例,为FIFO的简易设计,还没有考虑并发环境场景。

设计思路

1)用普通的hashMap保存缓存数据。
2)咱们须要额外的map用来保存key的过时特性,例子中使用了TreeMap,将“剩余存活时间”做为key,利用treemap的排序特性。

public class FIFOCache {  
  
    //按照访问时间排序,保存全部key-value  
    private final Map<String,Value> CACHE = new LinkedHashMap<>();  
  
    //过时数据,只保存有过时时间的key  
    //暂不考虑并发,咱们认为同一个时间内没有重复的key,若是改造的话,能够将value换成set  
    private final TreeMap<Long, String> EXPIRED = new TreeMap<>();  
  
    private final int capacity;  
  
    public FIFOCache(int capacity) {  
        this.capacity = capacity;  
    }  
  
    public Object get(String key) {  
        //  
        Value value = CACHE.get(key);  
        if (value == null) {  
            return null;  
        }  
  
        //若是不包含过时时间  
        long expired = value.expired;  
        long now = System.nanoTime();  
        //已过时  
        if (expired > 0 && expired <= now) {  
            CACHE.remove(key);  
            EXPIRED.remove(expired);  
            return null;  
        }  
        return value.value;  
    }  
  
    public void put(String key,Object value) {  
        put(key,value,-1);  
    }  
  
  
    public void put(String key,Object value,int seconds) {  
        //若是容量不足,移除过时数据  
        if (capacity < CACHE.size()) {  
            long now = System.nanoTime();  
            //有过时的,所有移除  
            Iterator<Long> iterator = EXPIRED.keySet().iterator();  
            while (iterator.hasNext()) {  
                long _key = iterator.next();  
                //若是已过时,或者容量仍然溢出,则删除  
                if (_key > now) {  
                    break;  
                }  
                //一次移除全部过时key  
                String _value = EXPIRED.get(_key);  
                CACHE.remove(_value);  
                iterator.remove();  
            }  
        }  
  
        //若是仍然容量不足,则移除最先访问的数据  
        if (capacity < CACHE.size()) {  
            Iterator<String> iterator = CACHE.keySet().iterator();  
            while (iterator.hasNext() && capacity < CACHE.size()) {  
                String _key = iterator.next();  
                Value _value = CACHE.get(_key);  
                long expired = _value.expired;  
                if (expired > 0) {  
                    EXPIRED.remove(expired);  
                }  
                iterator.remove();  
            }  
        }  
  
        //若是此key已存在,移除旧数据  
        Value current = CACHE.remove(key);  
        if (current != null && current.expired > 0) {  
            EXPIRED.remove(current.expired);  
        }  
        //若是指定了过时时间  
        if(seconds > 0) {  
            long expireTime = expiredTime(seconds);  
            EXPIRED.put(expireTime,key);  
            CACHE.put(key,new Value(expireTime,value));  
        } else {  
            CACHE.put(key,new Value(-1,value));  
        }  
  
    }  
  
    private long expiredTime(int expired) {  
        return System.nanoTime() + TimeUnit.SECONDS.toNanos(expired);  
    }  
  
    public void remove(String key) {  
        Value value = CACHE.remove(key);  
        if(value == null) {  
            return;  
        }  
        long expired = value.expired;  
        if (expired > 0) {  
            EXPIRED.remove(expired);  
        }  
    }  
  
  
    class Value {  
        long expired; //过时时间,纳秒  
        Object value;  
        Value(long expired,Object value) {  
            this.expired = expired;  
            this.value = value;  
        }  
    }  
}  
复制代码

LRU

least recently used,最近最少使用,是目前最经常使用的缓存算法和设计方案之一,其移除策略为“当缓存(页)满时,优先移除最近最久未使用的数据”,优势是易于设计和使用,适用场景普遍。算法能够参考leetcode 146 (LRU Cache)。

特色

1)Object get(key):从canche中获取key对应的数据,若是此key已过时,移除此key,并则返回null。
2)void put(key,value,expired):设置k-v,若是容量不足,则根据LRU置换算法移除“最久未被使用的key”,须要注意,根据LRU优先移除已过时的keys,若是没有,则根据LRU移除未过时的key。若是未设定过时时间,则认为永不自动过时。
3)此题,设计关键是过时时间特性,这与常规的LRU有所不一样。毕竟“过时时间”特性在cache设计中是必要的。

设计思路

1)LRU的基础算法,须要了解;每次put、get时须要更新key对应的访问时间,咱们须要一个数据结构可以保存key最近的访问时间且可以排序。
2)既然包含过时时间特性,那么带有过时时间的key须要额外的数据结构保存。
3)暂时不考虑并发操做;尽可能兼顾空间复杂度和时间复杂度。
4)此题仍然偏向于设计题,而非纯粹的算法题。
此题代码与FIFO基本相同,惟一不一样点为get()方法,对于LRU而言,get方法须要重设访问时间(即调整所在cache中顺序)

public Object get(String key) {  
    //  
    Value value = CACHE.get(key);  
    if (value == null) {  
        return null;  
    }  
  
    //若是不包含过时时间  
    long expired = value.expired;  
    long now = System.nanoTime();  
    //已过时  
    if (expired > 0 && expired <= now) {  
        CACHE.remove(key);  
        EXPIRED.remove(expired);  
        return null;  
    }  
    //相对于FIFO,增长顺序重置  
    CACHE.remove(key);  
    CACHE.put(key,value);  
    return value.value;  
}  
复制代码

LFU

最近最不经常使用,当缓存容量满时,移除访问次数最少的元素,若是访问次数相同的元素有多个,则移除最久访问的那个。设计要求参见leetcode 460( LFU Cache)

public class LFUCache {  
  
    //主要容器,用于保存k-v  
    private Map<String, Object> keyToValue = new HashMap<>();  
  
    //记录每一个k被访问的次数  
    private Map<String, Integer> keyToCount = new HashMap<>();  
  
    //访问相同次数的key列表,按照访问次数排序,value为相同访问次数到key列表。  
    private TreeMap<Integer, LinkedHashSet<String>> countToLRUKeys = new TreeMap<>();  
  
    private int capacity;  
  
    public LFUCache(int capacity) {  
        this.capacity = capacity;  
        //初始化,默认访问1次,主要是解决下文  
    }  
  
    public Object get(String key) {  
        if (!keyToValue.containsKey(key)) {  
            return null;  
        }  
  
        touch(key);  
        return keyToValue.get(key);  
    }  
  
    /** 
     * 若是一个key被访问,应该将其访问次数调整。 
     * @param key 
     */  
    private void touch(String key) {  
        int count = keyToCount.get(key);  
        keyToCount.put(key, count + 1);//访问次数增长  
        //从原有访问次数统计列表中移除  
        countToLRUKeys.get(count).remove(key);  
  
        //若是符合最少调用次数到key统计列表为空,则移除此调用次数到统计  
        if (countToLRUKeys.get(count).size() == 0) {  
            countToLRUKeys.remove(count);  
        }  
  
        //而后将此key的统计信息加入到管理列表中  
        LinkedHashSet<String> countKeys = countToLRUKeys.get(count + 1);  
        if (countKeys == null) {  
            countKeys = new LinkedHashSet<>();  
            countToLRUKeys.put(count + 1,countKeys);  
        }  
        countKeys.add(key);  
    }  
  
    public void put(String key, Object value) {  
        if (capacity <= 0) {  
            return;  
        }  
  
        if (keyToValue.containsKey(key)) {  
            keyToValue.put(key, value);  
            touch(key);  
            return;  
        }  
        //容量超额以后,移除访问次数最少的元素  
        if (keyToValue.size() >= capacity) {  
            Map.Entry<Integer,LinkedHashSet<String>> entry = countToLRUKeys.firstEntry();  
            Iterator<String> it = entry.getValue().iterator();  
            String evictKey = it.next();  
            it.remove();  
            if (!it.hasNext()) {  
                countToLRUKeys.remove(entry.getKey());  
            }  
            keyToCount.remove(evictKey);  
            keyToValue.remove(evictKey);  
  
        }  
  
        keyToValue.put(key, value);  
        keyToCount.put(key, 1);  
        LinkedHashSet<String> keys = countToLRUKeys.get(1);  
        if (keys == null) {  
            keys = new LinkedHashSet<>();  
            countToLRUKeys.put(1,keys);  
        }  
        keys.add(key);  
    }  
}  
复制代码

End

更加易用的cache,能够参考guava的实现。但愿这三个代码模版,可以对你有所帮助。

相关文章
相关标签/搜索