LRU缓存的实现


LRU简介

LRU是“Least Recently Used”的简写,意思是最近最少使用,是一种缓存淘汰策略,在有限的缓存资源中,淘汰掉最近最久未使用的。例如:缓存最大容纳10000条数据,在添加时,只要数据总数小于等于10000可以随意添加,但是当数据量大于1万时,将旧的数据删除,再添加新的数据;添加时,要将新来的数据添加到最前面。说完来添加,再来说查询,从缓存中获取的数据返回之前,需要将数据移动到最前面,因为获取改数据就代表最近使用了,对于缓存来说,最近这个数据很有可能会被再次使用,所以要移动到最前面。

LRU


LRU算法分析

因为缓存具有查询快(时间复杂度必须是O(1)),增加快,删除快的特点。同时,LRU策略分析的数据有新旧之分,新的在最前面,旧的往后放。因此选取数据结构时,要遵循有序的特点。因此可以将HashMap与双向链表结合起来实现LRUCache。Hash表查询效率是O(1),但是不是有序的,双向链表(此处不是循环链表)有序并且增加与删除都是O(1),但查询是O(n),所以两者互相取长补短。

在这里插入图片描述

实现代码

节点类

public class Node {
    public int key,value;
    public Node prev;
    public Node next;

    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }

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

双向链表

public class DoubleList {
    private LinkedList<Node> linkedList = new LinkedList();

    /**添加第一个节点
     * @param node
     */
    public void addFirst(Node node){
        linkedList.addFirst(node);
    }


    /**
     *移除最后一个节点
     */
    public Node removeLast(){
        return linkedList.removeLast();
    }


    /**
     *移除节点
     */
    public void remove(Node node){
        linkedList.remove(node);
    }


    /**
     *  链表长度
     * @return
     */
    public int size(){
        return linkedList.size();
    }

    /**
     * 输出双向链表
     */
    public void printAll(){
        Iterator<Node> iterator = linkedList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

LRUCache类

public class LRUCache {
    private HashMap<Integer, Node> map;
    private DoubleList cache;
    private int cap;

    public LRUCache( int cap) {
        this.map = new HashMap<Integer, Node>();
        this.cache = new DoubleList();
        this.cap = cap;
    }
	
	/**
	 * 查询value
	 **/
    public int get(int key){
        // 首先判断key是否在map中
        if (map.containsKey(key)){
            int x = map.get(key).value;
            // 在返回之前,将最近访问的键值对添加到LRU中
            put(key,x);
            return x;
        }else {
            // 不存在返回-1
            return -1;
        }
    }
	
	// 新增键值对
    public void put(int key,int value){
        Node node = new Node(key, value);
        // 首先判断key是否在map中,如果已存在,map直接新增,覆盖旧的value
        if (map.containsKey(key)){
            // 链表中删除旧的节点,在头部添加新的节点
            cache.remove(map.get(key));
            cache.addFirst(node);
            map.put(key,node);

        }else{
            // 在向LRU添加key时,如果缓存已满,要删除最后一个Node,同时map中也要记得删除该key
            if (cache.size()==cap){
                Node last = cache.removeLast();
                map.remove(last.key);
            }
            cache.addFirst(node);
            map.put(key,node);
        }

    }
	
	// 输出缓存中的数据
   public void listAll(){
        cache.printAll();
   }

}

测试类

class LRUCacheTest {
    private LRUCache lruCache = new LRUCache(4);

    @Test
    public void LRUCacheTest() {
        lruCache.put(1, 2);
        lruCache.put(2, 3);
        lruCache.put(3, 4);
        lruCache.put(5, 6);
        // 因为cache大小是4,再添加新的数据时,会将最早加入缓存的数据淘汰掉
        lruCache.put(6, 67);
        // 获取第二次添加的数据,获取后(2,3)会被移动到最前边
        lruCache.get(2);
        lruCache.listAll();
    }

}

所以输出结果:
在这里插入图片描述

总结

对于LRU算法,他的优势在于对热点数据的查询,使用LRU可以有效提高查询效率,但是对于批量查询历史数据,有可能导致缓存污染,偶然的查询历史数据,会覆盖掉热点数据,导致本次查询不再走缓存。

posted on 2020-08-21 19:18  青山是谁  阅读(16)  评论(0编辑  收藏  举报