手写LFU缓存淘汰算法

137 阅读2分钟

LFU

LFU 是一个缓存淘汰算法,全称是 Least Frequently Used,最少频次使用,也就是使用次数最少的缓存,会被优先淘汰。

分析

核心关注点:

  1. 需要实现一个缓存,用来存数据,KV结构是最优选择。
  2. 需要淘汰使用频率的缓存,所以需要记录K的访问频次和访问频次对应的K。

实现

算法的核心逻辑是在写入和查询缓存的时候,记录缓存的访问记录,从而在容量满的时候,将最近没被使用的缓存淘汰掉。

手动实现

实现起来可以用两个 HashMap 实现,一个记录缓存,一个记录频次。

public class Main {

    /**
     * 手写LFU缓存
     *
     * LFU 是一个最近最少使用频次的缓存淘汰算法
     * 也就是淘汰最近用的次数的缓存
     */
    public static void main(String[] args) {
        LFU<String> LFU = new LFU<>(3);
        LFU.put("a", "a");
        LFU.put("b", "b");
        LFU.put("c", "c");
        System.out.println(LFU.get("b"));
        System.out.println(LFU.get("c"));
        LFU.put("d", "d");
        System.out.println(LFU.get("a"));
    }

    static class LFU<T> {
        private final int capacity; // 容量
        private final HashMap<String, Node<T>> cache; // k => value + freq
        private final HashMap<Integer, LinkedList<String>> queue; // freq => key list
        priv

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

        public synchronized T get(String key) {
            if (cache.containsKey(key)) {
                Node<T> node = cache.get(key);

                // 访问+1
                queue.get(node.getFreq()).remove(key); // 从当前频率中移除
                node.setFreq(node.getFreq() + 1); // 访问次数+1

                afterOpt(key, node);

                return node.value;
            }
            return null;
        }

        public synchronized void put(String key, T value) {
            Node<T> node;
            if (cache.containsKey(key)) {
                // 已经存在,访问+1
                node = cache.get(key);

                queue.get(node.getFreq()).remove(key); // 从当前频率中移除
                node.setFreq(node.getFreq() + 1); // 访问次数+1
            } else {
                node = new Node<>(value, 1);
            }

            // 更新缓存
            this.cache.put(key, node);

            afterOpt(key, node);
        }

        private void afterOpt(String key, Node<T> node) {
            // 加入到新的频率中
            LinkedList<String> nodeList = queue.getOrDefault(node.getFreq(), new LinkedList<>());
            nodeList.add(key);
            queue.put(node.getFreq(), nodeList);

            if (cache.size() > capacity) {
                Integer minFreq = queue.keySet().stream().min(Integer::compareTo).get();
                // 移除最不常用的数据
                String removeKey = queue.get(minFreq).removeFirst();
                cache.remove(removeKey);

                // 清理频率残留
                if (queue.get(minFreq).isEmpty()) {
                    queue.remove(minFreq);
                }
            }
        }
    }

    /**
     * 缓存数据
     */
    static class Node<T> {
        private T value;
        private Integer freq;

        public Node(T value, Integer freq) {
            this.value = value;
            this.freq = freq;
        }

        public T getValue() {
            return value;
        }

        public Integer getFreq() {
            return freq;
        }

        public void setFreq(Integer freq) {
            this.freq = freq;
        }
    }
}

以上代码实现了一个基础的同步 LFU 缓存。

继续优化的方向如下:

  1. 淘汰缓存时,获取最小频次,每次都需要遍历,空间复杂度O(n),性能不高。
  2. 淘汰周期是全周期,可能存在很久以前的高频缓存,现在已经过期的情况。