• 深入理解缓存之常见的缓存算法


    GitHub:https://github.com/JDawnF

    缓存算法,比较常见的是三种:

    • LRU(least recently used ,最近最少使用)

    • LFU(Least Frequently used ,最不经常使用)

    • FIFO(first in first out ,先进先出)

    手写 LRU 代码的实现

    手写 LRU 代码的实现,有多种方式。其中,最简单的是基于 LinkedHashMap 来实现,代码如下:

    class LRUCache<K, V> extends LinkedHashMap<K, V> {
        private final int CACHE_SIZE;
    
        /**
         * 传递进来最多能缓存多少数据
         * @param cacheSize 缓存大小
         */
        public LRUCache(int cacheSize) {
        // true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最后访问的放在尾部。
            super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
            CACHE_SIZE = cacheSize;
        }
    
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
            return size() > CACHE_SIZE;
        }
        
    }
    
     

    自我实现:

    实现一:

    • 采用了与 HashMap 一样的保存数据方式,只是自己手动实现了一个简易版。

    • 内部采用了一个队列来保存每次写入的数据。

    • 写入的时候判断缓存是否大于了阈值 N,如果满足则根据队列的 FIFO 特性将队列头的数据删除。因为队列头的数据肯定是最先放进去的。

    • 再开启了一个守护线程用于判断最先放进去的数据是否超期(因为就算超期也是最先放进去的数据最有可能满足超期条件。)

    • 设置为守护线程可以更好的表明其目的(最坏的情况下,如果是一个用户线程最终有可能导致程序不能正常退出,因为该线程一直在运行,守护线程则不会有这个情况。)

    以下代码:就是最近最少使用没有满足,删除的数据都是最先放入的数据。

    import com.google.common.util.concurrent.ThreadFactoryBuilder;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import java.util.Set;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    /**
     * Function:
     * 1.在做 key 生成 hashcode 时是用的 HashMap 的 hash 函数
     * 2.在做 put get 时,如果存在 key 相等时候为了简单没有去比较 equal 和 hashcode
     * 3.限制大小, map的最大size是1024, 超过1024后,就淘汰掉最久没有访问的kv 键值对, 当淘汰时,需要调用一个callback   lruCallback(K key, V value)
     * 是利用每次 put 都将值写入一个内部队列,这样只需要判断队列里的第一个即可。
     * 4.具备超时功能, 当键值对1小时内没有被访问, 就被淘汰掉, 当淘汰时, 需要调用一个callback   timeoutCallback(K key, V value);
     * 超时同理,单独开启一个守护进程来处理,取的是队列里的第一个 因为第一个是最早放进去的。
     * 但是像 HashMap 里的扩容,链表在超过阈值之类的没有考虑进来。
     */
    public class LRUAbstractMap extends java.util.AbstractMap {
        private final static Logger LOGGER = LoggerFactory.getLogger(LRUAbstractMap.class);
    
        //检查是否超期线程
        private ExecutorService checkTimePool ;
    
        //map 最大size
        private final static int MAX_SIZE = 1024 ;
    
        private final static ArrayBlockingQueue<Node> QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ;
    
        // 默认大小
        private final static int DEFAULT_ARRAY_SIZE =1024 ;
        private int arraySize ;			// 数组大小
        private Object[] arrays ;		// 数组
        private volatile boolean flag = true ;		// 判断是否停止 flag
        private final static Long EXPIRE_TIME = 60 * 60 * 1000L ;	// 超时时间
        private volatile AtomicInteger size  ;		// 整个 Map 的大小
        
        public LRUAbstractMap() {
            arraySize = DEFAULT_ARRAY_SIZE;
            arrays = new Object[arraySize] ;
            //开启一个线程检查最先放入队列的值是否超期
            executeCheckTime();
        }
    
        /**
         * 开启一个线程检查最先放入队列的值是否超期 设置为守护线程
         */
        private void executeCheckTime() {
            ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                    .setNameFormat("check-thread-%d")
                    .setDaemon(true)
                    .build();
            checkTimePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
            checkTimePool.execute(new CheckTimeThread()) ;
        }
    
        @Override
        public Set<Entry> entrySet() {
            return super.keySet();
        }
    
        @Override
        public Object put(Object key, Object value) {
            int hash = hash(key);
            int index = hash % arraySize ;
            Node currentNode = (Node) arrays[index] ;
            if (currentNode == null){
                arrays[index] = new Node(null,null, key, value);
                //写入队列
                QUEUE.offer((Node) arrays[index]) ;
                sizeUp();
            }else {
                Node cNode = currentNode ;
                Node nNode = cNode ;
                if (nNode.key == key){		//存在就覆盖
                    cNode.val = value ;
                }
    
                while (nNode.next != null){
                    //key 存在 就覆盖 简单判断
                    if (nNode.key == key){
                        nNode.val = value ;
                        break ;
                    }else {
                        //不存在就新增链表
                        sizeUp();
                        Node node = new Node(nNode,null,key,value) ;
                        //写入队列
                        QUEUE.offer(currentNode) ;
                        cNode.next = node ;
                    }
                    nNode = nNode.next ;
                }
            }
            return null ;
        }
    
        @Override
        public Object get(Object key) {
            int hash = hash(key) ;
            int index = hash % arraySize ;
            Node currentNode = (Node) arrays[index] ;
            if (currentNode == null){
                return null ;
            }
            if (currentNode.next == null){
                currentNode.setUpdateTime(System.currentTimeMillis());	//更新时间
                return currentNode ;		 //没有冲突
            }
            Node nNode = currentNode ;
            while (nNode.next != null){
                if (nNode.key == key){
                    currentNode.setUpdateTime(System.currentTimeMillis());	//更新时间
                    return nNode ;
                }
                nNode = nNode.next ;
            }
            return super.get(key);
        }
    
    
        @Override
        public Object remove(Object key) {
            int hash = hash(key) ;
            int index = hash % arraySize ;
            Node currentNode = (Node) arrays[index] ;
            if (currentNode == null){
                return null ;
            }
            if (currentNode.key == key){
                sizeDown();
                arrays[index] = null ;
                QUEUE.poll();		//移除队列
                return currentNode ;
            }
            Node nNode = currentNode ;
            while (nNode.next != null){
                if (nNode.key == key){
                    sizeDown();
                    //在链表中找到了 把上一个节点的 next 指向当前节点的下一个节点
                    nNode.pre.next = nNode.next ;
                    nNode = null ;
                    QUEUE.poll();		//移除队列
                    return nNode;
                }
                nNode = nNode.next ;
            }
            return super.remove(key);
        }
    
        // 扩容
        private void sizeUp(){
            flag = true ;		//在put值时候认为里边已经有数据了
            if (size == null){
                size = new AtomicInteger() ;
            }
            int size = this.size.incrementAndGet();
            if (size >= MAX_SIZE) {
                //找到队列头的数据
                Node node = QUEUE.poll() ;
                if (node == null){
                    throw new RuntimeException("data error") ;
                }
                //移除该 key
                Object key = node.key ;
                remove(key) ;
                lruCallback() ;
            }
        }
    
       // 缩容
        private void sizeDown(){
            if (QUEUE.size() == 0){
                flag = false ;
            }
            this.size.decrementAndGet() ;
        }
    
        @Override
        public int size() {
            return size.get() ;
        }
    
       //链表
        private class Node{
            private Node next ;
            private Node pre ;
            private Object key ;
            private Object val ;
            private Long updateTime ;
            public Node(Node pre,Node next, Object key, Object val) {
                this.pre = pre ;
                this.next = next;
                this.key = key;
                this.val = val;
                this.updateTime = System.currentTimeMillis() ;
            }
    
            public void setUpdateTime(Long updateTime) {
                this.updateTime = updateTime;
            }
    
            public Long getUpdateTime() {
                return updateTime;
            }
    
            @Override
            public String toString() {
                return "Node{" +
                        "key=" + key +
                        ", val=" + val +
                        '}';
            }
        }
    
        /**
         * copy HashMap 的 hash 实现
         * @param key
         * @return
         */
        public int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    
        private void lruCallback(){
            LOGGER.debug("lruCallback");
        }
    
    
        private class CheckTimeThread implements Runnable{
            @Override
            public void run() {
                while (flag){
                    try {
                        Node node = QUEUE.poll();
                        if (node == null){
                            continue ;
                        }
                        Long updateTime = node.getUpdateTime() ;
    
                        if ((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){
                            remove(node.key) ;
                        }
                    } catch (Exception e) {
                        LOGGER.error("InterruptedException");
                    }
                }
            }
        }
    }

    实现二

    • 要记录最近最少使用,那至少需要一个有序的集合来保证写入的顺序。

    • 在使用了数据之后能够更新它的顺序。

    基于以上两点很容易想到一个常用的数据结构:双向链表

    1. 每次写入数据时将数据放入链表头结点。

    2. 使用数据时候将数据移动到头结点

    3. 缓存数量超过阈值时移除链表尾部数据。

    public class LRUMap<K, V> {
        private final Map<K, V> cacheMap = new HashMap<>();
    
        //最大缓存大小
        private int cacheSize;
        //节点大小
        private int nodeCount;
        // 头结点
        private Node<K, V> header;
        // 尾结点
        private Node<K, V> tailer;
    
        public LRUMap(int cacheSize) {
            this.cacheSize = cacheSize;
            //头结点的下一个结点为空
            header = new Node<>();
            header.next = null;
            //尾结点的上一个结点为空
            tailer = new Node<>();
            tailer.tail = null;
            //双向链表 头结点的上结点指向尾结点
            header.tail = tailer;
            //尾结点的下结点指向头结点
            tailer.next = header;
        }
    
        public void put(K key, V value) {
            cacheMap.put(key, value);
            //双向链表中添加结点,写入头节点
            addNode(key, value);
        }
    
        public V get(K key){
            Node<K, V> node = getNode(key);
            //移动到头结点
            moveToHead(node) ;
            return cacheMap.get(key);
        }
    
        private void moveToHead(Node<K,V> node){
            //如果是最后的一个节点
            if (node.tail == null){
                node.next.tail = null ;
                tailer = node.next ;
                nodeCount -- ;
            }
            //如果是本来就是头节点 不作处理
            if (node.next == null){
                return ;
            }
    
            //如果处于中间节点
            if (node.tail != null && node.next != null){
                //它的上一节点指向它的下一节点 也就删除当前节点
                node.tail.next = node.next ;
                node.next.tail = node.tail;
                nodeCount -- ;
            }
    
            //最后在头部增加当前节点
            //注意这里需要重新 new 一个对象,不然原本的node 还有着下面的引用,会造成内存溢出。
            node = new Node<>(node.getKey(),node.getValue()) ;
            addHead(node) ;
        }
    
        /**
         * 链表查询 效率较低
         * @param key
         * @return
         */
        private Node<K,V> getNode(K key){
            Node<K,V> node = tailer ;
            while (node != null){
                if (node.getKey().equals(key)){
                    return node ;
                }
                node = node.next ;
            }
            return null ;
        }
    
        /**
         * 写入头结点
         * @param key
         * @param value
         */
        private void addNode(K key, V value) {
            Node<K, V> node = new Node<>(key, value);
            //容量满了删除最后一个
            if (cacheSize == nodeCount) {
                //删除尾结点
                delTail();
            }
            //写入头结点
            addHead(node);
        }
        
        /**
         * 添加头结点
         * @param node
         */
        private void addHead(Node<K, V> node) {
            //写入头结点
            header.next = node;
            node.tail = header;
            header = node;
            nodeCount++;
            //如果写入的数据大于2个 就将初始化的头尾结点删除
            if (nodeCount == 2) {
                tailer.next.next.tail = null;
                tailer = tailer.next.next;
            }
        }
    
        private void delTail() {
            //把尾结点从缓存中删除
            cacheMap.remove(tailer.getKey());
            //删除尾结点
            tailer.next.tail = null;
            tailer = tailer.next;
            nodeCount--;
        }
    
        private class Node<K, V> {
            private K key;
            private V value;
            Node<K, V> tail;
            Node<K, V> next;
            public Node(K key, V value) {
                this.key = key;
                this.value = value;
            }
            public Node() {
            }
            public K getKey() {
                return key;
            }
            public void setKey(K key) {
                this.key = key;
            }
            public V getValue() {
                return value;
            }
            public void setValue(V value) {
                this.value = value;
            }
        }
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder() ;
            Node<K,V> node = tailer ;
            while (node != null){
                sb.append(node.getKey()).append(":")
                        .append(node.getValue())
                        .append("-->") ;
                node = node.next ;
            }
            return sb.toString();
        }
    }

    实际效果,写入时:

    @Test
     public void put() throws Exception {
         LRUMap<String,Integer> lruMap = new LRUMap(3) ;
         lruMap.put("1",1) ;
         lruMap.put("2",2) ;
         lruMap.put("3",3) ;
         System.out.println(lruMap.toString());
         lruMap.put("4",4) ;
         System.out.println(lruMap.toString());
         lruMap.put("5",5) ;
         System.out.println(lruMap.toString());
     }
    
    //输出:
    1:1-->2:2-->3:3-->
    2:2-->3:3-->4:4-->
    3:3-->4:4-->5:5-->

    使用时:

    @Test
     public void get() throws Exception {
         LRUMap<String,Integer> lruMap = new LRUMap(3) ;
         lruMap.put("1",1) ;
         lruMap.put("2",2) ;
         lruMap.put("3",3) ;
         System.out.println(lruMap.toString());
         System.out.println("==============");
         Integer integer = lruMap.get("1");
         System.out.println(integer);
         System.out.println("==============");
         System.out.println(lruMap.toString());
     }
    //输出
    1:1-->2:2-->3:3-->
    ==============
    1
    ==============
    2:2-->3:3-->1:1-->
    • 数据是直接利用 HashMap 来存放的。

    • 内部使用了一个双向链表来存放数据,所以有一个头结点 header,以及尾结点 tailer。

    • 每次写入头结点,删除尾结点时都是依赖于 header tailer。

    • 使用数据移动到链表头时,第一步是需要在双向链表中找到该节点。这里就体现出链表的问题了。查找效率很低,最差需要 O(N)。之后依赖于当前节点进行移动。

    • 在写入头结点时有判断链表大小等于 2 时需要删除初始化的头尾结点。这是因为初始化时候生成了两个双向节点,没有数据只是为了形成一个数据结构。当真实数据进来之后需要删除以方便后续的操作(这点可以继续优化)。

    • 以上的所有操作都是线程不安全的,需要使用者自行控制。

    初始化时

    写入数据时

    LRUMap<String,Integer> lruMap = new LRUMap(3) ;
    lruMap.put("1",1) ;
    lruMap.put("2",2) ;
    lruMap.put("3",3) ;
    lruMap.put("4",4) ;

     获取数据时

    Integer integer = lruMap.get("2");

    参照:《动手实现一个 LRU Cache》

    在 Java 后端开发中,常见的缓存工具和框架列举如下:

    • 本地缓存:Guava LocalCache、Ehcache、Caffeine 。

      • Ehcache 的功能更加丰富,Caffeine 的性能要比 Guava LocalCache 好。

    • 分布式缓存:Redis、MemCache、Tair 。

      • Redis 最为主流和常用。

    参照:芋道源码

  • 相关阅读:
    Leetcode: 1425
    Leetcode: 1508 Range Sum of Sorted Subarray Sums
    Leetcode: 1353. Maximum Number of Events That Can Be Attended
    Leetcode: 1424. Diagonal Traverse II
    Leetcode: 825. Friends Of Appropriate Ages
    非递归实现二叉树的前序,中序,后序遍历
    TCP协议详解
    Linux常见命令
    C++基础笔记
    指针和引用的区别
  • 原文地址:https://www.cnblogs.com/baichendongyang/p/13235431.html
Copyright © 2020-2023  润新知