文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Redis:内存被我用完了!该怎么办?

2024-12-03 12:26

关注

介绍

Redis是一个内存数据库,当Redis使用的内存超过物理内存的限制后,内存数据会和磁盘产生频繁的交换,交换会导致Redis性能急剧下降。所以在生产环境中我们通过配置参数maxmemoey来限制使用的内存大小。

当实际使用的内存超过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算法

「lru(Least recently used,最近最少使用)算法,即最近访问的数据,后续很大概率还会被访问到,即是有用的。而长时间未被访问的数据,应该被淘汰」

lru算法中数据会被放到一个链表中,链表的头节点为最近被访问的数据,链表的尾节点为长时间没有被访问的数据

「lru算法的核心实现就是哈希表加双向链表」。链表可以用来维护访问元素的顺序,而hash表可以帮我们在O(1)时间复杂度下访问到元素。

「至于为什么是双向链表呢」?主要是要删除元素,所以要获取前继节点。数据结构图示如下

使用双向链表+HashMap

双向链表节点定义如下

  1. public class ListNode { 
  2.     K key
  3.     V value; 
  4.     ListNode pre; 
  5.     ListNode next
  6.  
  7.     public ListNode() {} 
  8.  
  9.     public ListNode(K key, V value) { 
  10.         this.key = key
  11.         this.value = value; 
  12.     } 

封装双向链表的常用操作

  1. public class DoubleList { 
  2.  
  3.     private ListNode head; 
  4.     private ListNode tail; 
  5.  
  6.     public DoubleList() { 
  7.         head = new ListNode(); 
  8.         tail = new ListNode(); 
  9.         head.next = tail; 
  10.         tail.pre = head; 
  11.     } 
  12.  
  13.     public void remove(ListNode node) { 
  14.         node.pre.next = node.next
  15.         node.next.pre = node.pre; 
  16.     } 
  17.  
  18.     public void addLast(ListNode node) { 
  19.         node.pre = tail.pre; 
  20.         tail.pre = node; 
  21.         node.pre.next = node; 
  22.         node.next = tail; 
  23.     } 
  24.  
  25.     public ListNode removeFirst() { 
  26.         if (head.next == tail) { 
  27.             return null
  28.         } 
  29.         ListNode first = head.next
  30.         remove(first); 
  31.         return first
  32.     } 

封装一个缓存类,提供最基本的get和put方法。「需要注意,这两种基本的方法都涉及到对两种数据结构的修改」。

  1. public class MyLruCache { 
  2.  
  3.     private int capacity; 
  4.     private DoubleList doubleList; 
  5.     private Map map; 
  6.  
  7.     public MyLruCache(int capacity) { 
  8.         this.capacity = capacity; 
  9.         map = new HashMap<>(); 
  10.         doubleList = new DoubleList(); 
  11.     } 
  12.  
  13.     public V get(Object key) { 
  14.         ListNode node = map.get(key); 
  15.         if (node == null) { 
  16.             return null
  17.         } 
  18.         // 先删除该节点,再接到尾部 
  19.         doubleList.remove(node); 
  20.         doubleList.addLast(node); 
  21.         return node.value; 
  22.     } 
  23.  
  24.     public void put(K key, V value) { 
  25.         // 直接调用这边的get方法,如果存在,它会在get内部被移动到尾巴,不用再移动一遍,直接修改值即可 
  26.         if ((get(key)) != null) { 
  27.             map.get(key).value = value; 
  28.             return
  29.         } 
  30.  
  31.         // 如果超出容量,把头去掉 
  32.         if (map.size() == capacity) { 
  33.             ListNode listNode = doubleList.removeFirst(); 
  34.             map.remove(listNode.key); 
  35.         } 
  36.  
  37.         // 若不存在,new一个出来 
  38.         ListNode node = new ListNode(key, value); 
  39.         map.put(key, node); 
  40.         doubleList.addLast(node); 
  41.     } 

这里我们的实现为最近访问的放在链表的尾节点,不经常访问的放在链表的头节点

测试一波,输出为链表的正序输出(代码为了简洁没有贴toString方法)

  1. MyLruCache myLruCache = new MyLruCache<>(3); 
  2. // {5 : 5} 
  3. myLruCache.put("5""5"); 
  4. // {5 : 5}{3 : 3} 
  5. myLruCache.put("3""3"); 
  6. // {5 : 5}{3 : 3}{4 : 4} 
  7. myLruCache.put("4""4"); 
  8. // {3 : 3}{4 : 4}{2 : 2} 
  9. myLruCache.put("2""2"); 
  10. // {4 : 4}{2 : 2}{3 : 3} 
  11. myLruCache.get("3"); 

「因为LinkedHashMap的底层实现就是哈希表加双向链表,所以你可以用LinkedHashMap替换HashMap和DoubleList来改写一下上面的类」。

我来演示一下更骚的操作,只需要重写一个构造函数和removeEldestEntry方法即可。

使用LinkedHashMap实现LRU

  1. public class LruCache extends LinkedHashMap { 
  2.  
  3.     private int cacheSize; 
  4.  
  5.  
  6.     public LruCache(int cacheSize) { 
  7.          
  8.         super(cacheSize, 0.75f, true); 
  9.         this.cacheSize = cacheSize; 
  10.     } 
  11.  
  12.      
  13.     @Override 
  14.     protected boolean removeEldestEntry(Map.Entry eldest) { 
  15.         return size() > cacheSize; 
  16.     } 

注意这个缓存并不是线程安全的,可以调用Collections.synchronizedMap方法返回线程安全的map

  1. LruCache lruCache = new LruCache(3); 
  2. Map safeMap = Collections.synchronizedMap(lruCache); 

Collections.synchronizedMap实现线程安全的方式很简单,只是返回一个代理类。代理类对Map接口的所有方法加锁

  1. public static  Map synchronizedMap(Map m) { 
  2.     return new SynchronizedMap<>(m); 

LFU算法

「LRU算法有一个问题,当一个长时间不被访问的key,偶尔被访问一下后,可能会造成一个比这个key访问更频繁的key被淘汰。」

即LRU算法对key的冷热程度的判断可能不准确。而LFU算法(Least Frequently Used,最不经常使用)则是按照访问频率来判断key的冷热程度的,每次删除的是一段时间内访问频率较低的数据,比LRU算法更准确。

使用3个hash表实现lfu算法

那么我们应该如何组织数据呢?

为了实现键值的对快速访问,用一个map来保存键值对

  1. private HashMapInteger> keyToFreq; 

还需要用一个map来保存键的访问频率

  1. private HashMapInteger> keyToFreq; 

「当然你也可以把值和访问频率封装到一个类中,用一个map来替代上述的2个map」

接下来就是最核心的部分,删除访问频率最低的数据。

下面就是具体的实现。

  1. public class LfuCache { 
  2.  
  3.     private HashMap keyToVal; 
  4.     private HashMapInteger> keyToFreq; 
  5.     private HashMap<Integer, LinkedHashSet> freqTokeys; 
  6.  
  7.     private int minFreq; 
  8.     private int capacity; 
  9.  
  10.     public LfuCache(int capacity) { 
  11.         keyToVal = new HashMap<>(); 
  12.         keyToFreq = new HashMap<>(); 
  13.         freqTokeys = new HashMap<>(); 
  14.         this.capacity = capacity; 
  15.         this.minFreq = 0; 
  16.     } 
  17.  
  18.     public V get(K key) { 
  19.         V v = keyToVal.get(key); 
  20.         if (v == null) { 
  21.             return null
  22.         } 
  23.         increaseFrey(key); 
  24.         return v; 
  25.     } 
  26.  
  27.     public void put(K key, V value) { 
  28.         // get方法里面会增加频次 
  29.         if (get(key) != null) { 
  30.             // 重新设置值 
  31.             keyToVal.put(key, value); 
  32.             return
  33.         } 
  34.  
  35.         // 超出容量,删除频率最低的key 
  36.         if (keyToVal.size() >= capacity) { 
  37.             removeMinFreqKey(); 
  38.         } 
  39.  
  40.         keyToVal.put(key, value); 
  41.         keyToFreq.put(key, 1); 
  42.         // key对应的value存在,返回存在的key 
  43.         // key对应的value不存在,添加key和value 
  44.         freqTokeys.putIfAbsent(1, new LinkedHashSet<>()); 
  45.         freqTokeys.get(1).add(key); 
  46.         this.minFreq = 1; 
  47.     } 
  48.  
  49.     // 删除出现频率最低的key 
  50.     private void removeMinFreqKey() { 
  51.         LinkedHashSet keyList = freqTokeys.get(minFreq); 
  52.         K deleteKey = keyList.iterator().next(); 
  53.         keyList.remove(deleteKey); 
  54.         if (keyList.isEmpty()) { 
  55.             // 这里删除元素后不需要重新设置minFreq 
  56.             // 因为put方法执行完会将minFreq设置为1 
  57.             freqTokeys.remove(keyList); 
  58.         } 
  59.         keyToVal.remove(deleteKey); 
  60.         keyToFreq.remove(deleteKey); 
  61.     } 
  62.  
  63.     // 增加频率 
  64.     private void increaseFrey(K key) { 
  65.         int freq = keyToFreq.get(key); 
  66.         keyToFreq.put(key, freq + 1); 
  67.         freqTokeys.get(freq).remove(key); 
  68.         freqTokeys.putIfAbsent(freq + 1, new LinkedHashSet<>()); 
  69.         freqTokeys.get(freq + 1).add(key); 
  70.         if (freqTokeys.get(freq).isEmpty()) { 
  71.             freqTokeys.remove(freq); 
  72.             // 最小频率的set为空,key被移动到minFreq+1对应的set了 
  73.             // 所以minFreq也要加1 
  74.             if (freq == this.minFreq) { 
  75.                 this.minFreq++; 
  76.             } 
  77.         } 
  78.     } 

测试一下

  1. LfuCache lfuCache = new LfuCache(2); 
  2. lfuCache.put("1""1"); 
  3. lfuCache.put("2""2"); 
  4. // 1 
  5. System.out.println(lfuCache.get("1")); 
  6. lfuCache.put("3""3"); 
  7. // 1的频率为2,2和3的频率为1,但2更早之前被访问,所以被清除 
  8. // 结果为null 
  9. System.out.println(lfuCache.get("2")); 

 

来源: Java识堂内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯