LFU
LFU 是一个缓存淘汰算法,全称是 Least Frequently Used,最少频次使用,也就是使用次数最少的缓存,会被优先淘汰。
分析
核心关注点:
- 需要实现一个缓存,用来存数据,KV结构是最优选择。
- 需要淘汰使用频率的缓存,所以需要记录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 缓存。
继续优化的方向如下:
- 淘汰缓存时,获取最小频次,每次都需要遍历,空间复杂度O(n),性能不高。
- 淘汰周期是全周期,可能存在很久以前的高频缓存,现在已经过期的情况。