缓存

缓存作用:优化性能,减少计算次数。减少数据库访问次数和压力。
缓存污染:将不常用的数据添加到缓存,降低缓存效率。
缓存淘汰:将不常用的缓存数据清理掉,避免内存溢出。

LRU(Least recently used)算法

根据数据历史访问记录淘汰缓存数据,
核心思想:“如果数据最近被访问过,那么将来被访问的几率也越高”。

一、实现思路:

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
LRU算法实现

  1. 添加新数据到链表头部。
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部。
  3. 当链表满时,丢弃链表尾部数据。

二、Java代码实现

为了快速查找缓存数据,本文示例使用哈希表存储缓存。
以下示例采用循环链表(包含头尾结点)和哈希表实现简单LRU算法。

import com.google.common.base.Objects;
import org.junit.Assert;

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

/**
 * 简单实现LRU缓存淘汰算法(基于循环链表和哈希表实现缓存历史访问列表)
 * 根据数据历史访问记录淘汰缓存数据(缓存命中率低,存在时间久的数据优先淘汰)
 *
 * @author genxinliu@hengtiansoft.com
 * @see Node 链表结点 循环链表(包含头尾结点,使代码更简洁)
 * @see HashMap 哈希表
 * @since 03.12.2019
 */
public class SimpleLRUCache<K, V> {

    static class Node<K, V> {
        Node<K, V> prev, next;
        final K key;
        V val;

        public Node() {
            this.key = null;
        }
        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }
        public Node(K key, V val, Node<K, V> prev, Node<K, V> next) {
            this.prev = prev;
            this.next = next;
            this.key = key;
            this.val = val;
        }

        public K getKey() {
            return key;
        }
        public V getVal() {
            return val;
        }
        public void setVal(V val) {
            this.val = val;
        }
        public Node<K, V> getPrev() {
            return prev;
        }
        public void setPrev(Node<K, V> node) {
            this.prev = node;
        }
        public Node<K, V> getNext() {
            return next;
        }
        public void setNext(Node<K, V> node) {
            this.next = node;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Node<?, ?> node = (Node<?, ?>) o;
            return Objects.equal(key, node.key);
        }
        @Override
        public int hashCode() {
            return Objects.hashCode(key);
        }
        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    '}';
        }
    }

    /** 链表头结点 */
    private final Node<K, V> head = new Node<K, V>();
    /** 链表尾结点 */
    private final Node<K, V> tail = new Node<K, V>();
    /** 链表长度 */
    private int limit;
    private Map<K, Node<K, V>> cache;

    public SimpleLRUCache(int limit) {
        this.limit = limit;
        cache = new HashMap<K, Node<K, V>>(limit);

        // 关联头尾结点
        head.prev = head.next = tail;
        tail.prev = tail.next = head;
    }

    public static void main(String[] args) {
        SimpleLRUCache<String, String> cache = new SimpleLRUCache<>(3);
        cache.put("1", "1");
        cache.put("2", "2");
        cache.put("3", "3");

        cache.put("4", "4");
        Assert.assertTrue("1 is not out!!!!", cache.get("1") == null);

        cache.get("2");
        cache.put("5", "5");
        Assert.assertTrue("2 is out!!!!", cache.get("2") != null);
    }

    /**
     * 新增缓存数据到哈希表和链表头部
     * 如果缓存已存在,则更新缓存数据
     * 如果已达到链表上限,则淘汰链表尾端的数据 再做新增处理
     *
     * @param key 缓存的key
     * @param val 缓存数据
     */
    public void put(K key, V val) {
        synchronized (cache) {
            if (cache.containsKey(key)) {
                // refresh val
                cache.get(key).setVal(val);
            } else {
                if (cache.size() >= limit) {
                    // remove tail
                    Node<K, V> tail = tail();
                    nodeRemoval(tail);
                    cache.remove(tail.getKey());
                }
                Node<K, V> node = new Node<>(key, val);
                // add first
                linkNodeFirst(node);
                // add cache
                cache.put(key, node);
            }
        }
    }

    /**
     * 获取缓存,如果命中缓存,则将数据添加到链表头部
     *
     * @param key
     */
    public V get(K key) {
        Node<K, V> crt = cache.get(key);
        if (crt != null) {
            // double check locking
            if(head() != crt) {
                synchronized (head) {
                    if (head() != crt) {
                        nodeRemoval(crt);
                        linkNodeFirst(crt);
                    }
                }
            }
            return crt.val;
        }
        return null;
    }

    // add dst before head.next
    private void linkNodeFirst(Node<K, V> dst) {
        linkNodeBefore(head.next, dst);
    }
    // add dst before tail
    private void linkNodeLast(Node<K, V> dst) {
        linkNodeBefore(tail, dst);
    }
    // add dst before src
    private void linkNodeBefore(Node<K, V> src, Node<K, V> dst) {
        Node<K, V> p = src.prev;
        p.next = dst;
        dst.prev = p;
        dst.next = src;
        src.prev = dst;
    }
    // dst replace src
    private void transferLinks(Node<K, V> src, Node<K, V> dst) {
        Node<K, V> p = src.prev, n = src.next;
        src.prev = src.next = null;

        p.next = dst;
        dst.prev = p;
        dst.next = n;
        n.prev = dst;
    }
    // remove dst
    private void nodeRemoval(Node<K, V> dst) {
        if (null != dst) {
            Node<K, V> p = dst.prev, n = dst.next;
            dst.prev = dst.next = null;
            p.next = n;
            n.prev = p;
        }
    }

    // return head.next. if is tail return null
    private Node<K, V> head() {
        return head.next == tail ? null : head.next;
    }
    // return tail.prev. if is head return null
    private Node<K, V> tail() {
        return tail.prev == head ? null : tail.prev;
    }
}

参考:Redis 缓存淘汰算法——LRU算法


roylion
204 声望25 粉丝

读书破万卷