一、概念
LRU(Least Recently Used,最近最少使用)算法和
LFU(Least Frequently Used,最不经常使用)算法
都是用来作为缓存淘汰机制,保证缓存空间的有效利用。
二、使用场景
适合使用 LRU 的场景:
- 适合使用 LRU 的场景是数据访问具有时间局部性的场景。
- 例如在Web网站中,用户最近访问的页面,很可能再次被访问。在这种情况下,缓存的内容一般都是最近访问的页面数据,而离当前时间较长的访问页面数据缓存命中率较小,很可能被清理出缓存,因此使用 LRU 算法清理缓存会比较合适。
- 另一个适合使用 LRU 的场景是数据库缓存。通常情况下,数据库表不会突然新增大量的记录导致缓存不够用,而是查询热点数据集中在很少几张表上。在这种情况下,使用 LRU 算法淘汰缓存可以比 LFU 更好地利用 LRU 的时间局部性,将数据进入缓存,提高缓存命中率。
适合使用 LFU 的场景:
- 适合使用 LFU 的场景是数据访问热度不均匀的场景。
- 例如在电商网站的热门商品推荐、搜索引擎中的热门搜索推荐等场景中,只有少量的热门数据集中访问比较多,而大量的数据很少被访问,使用 LRU 算法会出现所谓的「冷用户」现象,这时候使用 LFU 算法就能很好的有效清理低访问率的数据。
- 另一个适合使用 LFU 的场景是数据库缓存。当数据集合较大,且访问热度比较分散的时候,使用 LRU 的效果较差,因为 LRU 显然更倾向于缓存最近的数据,而 LFU 会更倾向于缓存访问频率高的数据。
综上,根据访问局部性和热度分布的不同,不同的应用场景会选择不同的缓存淘汰算法,以达到最优的缓存利用效果。
三、代码实现
LRU算法实现
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int MAX_CACHE_SIZE; // 缓存最大容量
public LRUCache(int maxCacheSize) {
// 第三个参数 accessOrder=true表示按照访问顺序而非插入顺序排序。
super((int) Math.ceil(maxCacheSize / 0.75) + 1, 0.75f, true);
MAX_CACHE_SIZE = maxCacheSize;
}
// 在LRUCache类中,我们通过覆盖removeEldestEntry方法来实现LRU算法的删除策略,当缓存元素长度超过设定的最大容量时,就将最久未使用的元素删除。
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
public static void main(String[] args) {
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("1", "one");
cache.put("2", "two");
cache.put("3", "three");
System.out.println(cache);
cache.get("2");
cache.put("4", "four");
System.out.println(cache);
cache.put("3", "new-three");
System.out.println(cache);
}
}
输出:
{1=one, 2=two, 3=three}
{2=two, 4=four, 3=three}
{2=two, 4=four, 3=new-three}
LFU算法实现
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
public class LFUCache {
// 记录key对应的value
private Map<Integer, Integer> valueMap = new HashMap<>();
// 记录key键的访问频率
private Map<Integer, Integer> freqMap = new HashMap<>();
// 记录每个频率下的key,使用LinkedHashSet保证添加和删除的效率
private Map<Integer, LinkedHashSet<Integer>> freqKeys = new HashMap<>();
// LFU缓存容量
private int capacity;
// 最小频率
private int minFreq = 0;
/**
* LFU缓存构造函数
* @param capacity 缓存容量
*/
public LFUCache(int capacity) {
this.capacity = capacity;
}
/**
* 获取LFU缓存中对应key的value值
* @param key 键
* @return 对应的值
*/
public int get(int key) {
// 如果缓存中没有这个key,返回-1
if (!valueMap.containsKey(key)) {
return -1;
}
// 更新key的访问频率并返回value值
int freq = freqMap.get(key);
freqMap.put(key, freq + 1);
freqKeys.get(freq).remove(key);
if (freq == minFreq && freqKeys.get(freq).isEmpty()) {
// 如果刚刚移除的是最小的频率,更新最小频率
minFreq++;
}
freqKeys.computeIfAbsent(freq + 1, l -> new LinkedHashSet<>()).add(key);
return valueMap.get(key);
}
/**
* 向LFU缓存中添加键值对
* @param key 键
* @param value 值
*/
public void put(int key, int value) {
if (capacity <= 0) {
return;
}
// 如果缓存中已经有这个key,更新value值
if (valueMap.containsKey(key)) {
valueMap.put(key, value);
get(key);
return;
}
// 缓存已满,移除访问频率最低的key
if (valueMap.size() >= capacity) {
int toRemove = freqKeys.get(minFreq).iterator().next();
freqKeys.get(minFreq).remove(toRemove);
valueMap.remove(toRemove);
freqMap.remove(toRemove);
}
// 添加新的键值对
valueMap.put(key, value);
freqMap.put(key, 1);
freqKeys.computeIfAbsent(1, l -> new LinkedHashSet<>()).add(key);
minFreq = 1;
}
/**
* 测试用例
* @param args 参数
*/
public static void main(String[] args) {
LFUCache cache = new LFUCache(3);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 1
cache.put(3, 3);
System.out.println(cache.get(2)); // -1
System.out.println(cache.get(3)); // 3
cache.put(4, 4);
System.out.println(cache.get(1)); // -1
System.out.println(cache.get(3)); // 3
System.out.println(cache.get(4)); // 4
}
}