当实际使用的内存超过maxmemoey后,Redis提供了如下几种可选策略。
noeviction:写请求返回错误
volatile-lru:使用lru算法删除设置了过期时间的键值对
volatile-lfu:使用lfu算法删除设置了过期时间的键值对
volatile-random:在设置了过期时间的键值对中随机进行删除
volatile-ttl:根据过期时间的先后进行删除,越早过期的越先被删除
allkeys-lru:在所有键值对中,使用lru算法进行删除
allkeys-lfu:在所有键值对中,使用lfu算法进行删除
allkeys-random:所有键值对中随机删除
我们来详细了解一下lru和lfu算法,这是2个常见的缓存淘汰算法。因为计算机缓存的容量是有限的,所以我们要删除那些没用的数据,而这两种算法的区别就是判定没用的纬度不一样。
lru(Least recently used,最近最少使用)算法,即最近访问的数据,后续很大概率还会被访问到,即是有用的。而长时间未被访问的数据,应该被淘汰
lru算法中数据会被放到一个链表中,链表的头节点为最近被访问的数据,链表的尾节点为长时间没有被访问的数据
lru算法的核心实现就是哈希表加双向链表。链表可以用来维护访问元素的顺序,而hash表可以帮我们在O(1)时间复杂度下访问到元素。
至于为什么是双向链表呢?主要是要删除元素,所以要获取前继节点。数据结构图示如下
双向链表节点定义如下
public class ListNode<K, V> {K key;V value;ListNode pre;ListNode next;public ListNode() {}public ListNode(K key, V value) {this.key = key;this.value = value;}
}
封装双向链表的常用操作
public class DoubleList {private ListNode head;private ListNode tail;public DoubleList() {head = new ListNode();tail = new ListNode(); = tail;tail.pre = head;}public void remove(ListNode node) { = ;pre = node.pre;}public void addLast(ListNode node) {node.pre = tail.pre;tail.pre = node; = = tail;}public ListNode removeFirst() {if ( == tail) {return null;}ListNode first = ;remove(first);return first;}
}
封装一个缓存类,提供最基本的get和put方法。需要注意,这两种基本的方法都涉及到对两种数据结构的修改。
public class MyLruCache<K, V> {private int capacity;private DoubleList doubleList;private Map<K, ListNode> map;public MyLruCache(int capacity) {this.capacity = capacity;map = new HashMap<>();doubleList = new DoubleList();}public V get(Object key) {ListNode<K, V> node = (key);if (node == null) {return null;}// 先删除该节点,再接到尾部ve(node);doubleList.addLast(node);return node.value;}public void put(K key, V value) {// 直接调用这边的get方法,如果存在,它会在get内部被移动到尾巴,不用再移动一遍,直接修改值即可if ((get(key)) != null) {(key).value = value;return;}// 如果超出容量,把头去掉if (map.size() == capacity) {ListNode listNode = veFirst();ve(listNode.key);}// 若不存在,new一个出来ListNode node = new ListNode(key, value);map.put(key, node);doubleList.addLast(node);}
}
这里我们的实现为最近访问的放在链表的尾节点,不经常访问的放在链表的头节点
测试一波,输出为链表的正序输出(代码为了简洁没有贴toString方法)
MyLruCache<String, String> myLruCache = new MyLruCache<>(3);
// {5 : 5}
myLruCache.put("5", "5");
// {5 : 5}{3 : 3}
myLruCache.put("3", "3");
// {5 : 5}{3 : 3}{4 : 4}
myLruCache.put("4", "4");
// {3 : 3}{4 : 4}{2 : 2}
myLruCache.put("2", "2");
// {4 : 4}{2 : 2}{3 : 3}
("3");
因为LinkedHashMap的底层实现就是哈希表加双向链表,所以你可以用LinkedHashMap替换HashMap和DoubleList来改写一下上面的类。
我来演示一下更骚的操作,只需要重写一个构造函数和removeEldestEntry方法即可。
public class LruCache<K, V> extends LinkedHashMap<K, V> {private int cacheSize;public LruCache(int cacheSize) {/*** initialCapacity: 初始容量大小* loadFactor: 负载因子* accessOrder: false基于插入排序(默认),true基于访问排序*/super(cacheSize, 0.75f, true);this.cacheSize = cacheSize;}/*** 当调用put或者putAll方法时会调用如下方法,是否删除最老的数据,默认为false*/@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > cacheSize;}
}
注意这个缓存并不是线程安全的,可以调用Collections.synchronizedMap方法返回线程安全的map
LruCache<String, String> lruCache = new LruCache(3);
Map<String, String> safeMap = Collections.synchronizedMap(lruCache);
Collections.synchronizedMap实现线程安全的方式很简单,只是返回一个代理类。代理类对Map接口的所有方法加锁
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);
}
LRU算法有一个问题,当一个长时间不被访问的key,偶尔被访问一下后,可能会造成一个比这个key访问更频繁的key被淘汰。
即LRU算法对key的冷热程度的判断可能不准确。而LFU算法(Least Frequently Used,最不经常使用)则是按照访问频率来判断key的冷热程度的,每次删除的是一段时间内访问频率较低的数据,比LRU算法更准确。
那么我们应该如何组织数据呢?
为了实现键值的对快速访问,用一个map来保存键值对
private HashMap<K, Integer> keyToVal;
还需要用一个map来保存键的访问频率
private HashMap<K, Integer> keyToFreq;
当然你也可以把值和访问频率封装到一个类中,用一个map来替代上述的2个map
接下来就是最核心的部分,删除访问频率最低的数据。
下面就是具体的实现。
public class LfuCache<K, V> {private HashMap<K, V> keyToVal;private HashMap<K, Integer> keyToFreq;private HashMap<Integer, LinkedHashSet<K>> freqTokeys;private int minFreq;private int capacity;public LfuCache(int capacity) {keyToVal = new HashMap<>();keyToFreq = new HashMap<>();freqTokeys = new HashMap<>();this.capacity = capacity;this.minFreq = 0;}public V get(K key) {V v = (key);if (v == null) {return null;}increaseFrey(key);return v;}public void put(K key, V value) {// get方法里面会增加频次if (get(key) != null) {// 重新设置值keyToVal.put(key, value);return;}// 超出容量,删除频率最低的keyif (keyToVal.size() >= capacity) {removeMinFreqKey();}keyToVal.put(key, value);keyToFreq.put(key, 1);// key对应的value存在,返回存在的key// key对应的value不存在,添加key和valuefreqTokeys.putIfAbsent(1, new LinkedHashSet<>());(1).add(key);this.minFreq = 1;}// 删除出现频率最低的keyprivate void removeMinFreqKey() {LinkedHashSet<K> keyList = (minFreq);K deleteKey = keyList.iterator().next();ve(deleteKey);if (keyList.isEmpty()) {// 这里删除元素后不需要重新设置minFreq// 因为put方法执行完会将minFreq设置为ve(keyList);}ve(deleteKey);ve(deleteKey);}// 增加频率private void increaseFrey(K key) {int freq = (key);keyToFreq.put(key, freq + 1);(freq).remove(key);freqTokeys.putIfAbsent(freq + 1, new LinkedHashSet<>());(freq + 1).add(key);if ((freq).isEmpty()) {ve(freq);// 最小频率的set为空,key被移动到minFreq+1对应的set了// 所以minFreq也要加1if (freq == this.minFreq) {this.minFreq++;}}}
}
测试一下
LfuCache<String, String> lfuCache = new LfuCache(2);
lfuCache.put("1", "1");
lfuCache.put("2", "2");
// 1
System.out.("1"));
lfuCache.put("3", "3");
// 1的频率为2,2和3的频率为1,但2更早之前被访问,所以被清除
// 结果为null
System.out.("2"));
lru算法和lfu算法
[1]
海子大佬的页面置换算法
[2].html
[3]leetcode lru算法
/
[4]leetcode lfu算法
[5]
lfu算法
[6]
本文发布于:2024-02-06 01:15:51,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/1712705182190651.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |