• Redis的过期策略和内存淘汰机制


    过期清除策略

    定期删除(redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删)+惰性删除(在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。)

    如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了,怎么办?
    答案是:走内存淘汰机制。

    内存淘汰机制

    1. 内存满时,直接报错。2. 移除最近最少使用的key。(LRU-Least Recently Used最常用)3.随机移除一个key。

    4.在设置了过期时间的key中,移除最近最少使用的key。5.在设置了过期时间的key中,随机移除一个key。6.在设置了过期时间的key中,更早过期时间的key优先移除。

    LRU的实现(哈希表提供查询,双向链表维持顺序和提供hash表中需要淘汰的key)

    1. LRU 算法设计

    我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。
    因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。 那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。
    所以结合一下,形成一种新的数据结构:哈希双向链表。 LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:

    思路
    由于题目的时间复杂度要求 O(1)O(1),空间肯定不能省,存取数据时间性能最好的就是哈希表,因此底层的数据结构一定是一个哈希表;
    根据题目意思,访问某个数据,时间优先级得提前,还有删除末尾结点的需求,这样的数据结构得在头尾访问数据最快,这种数据结构是「双向链表」;
    「链表」结点需要记录:1、value,2、key(在哈希表里删除的时候用得上),3、前驱结点引用,4、后继结点引用。
    这样一套设计下来,题目中要求的操作就是 $O(1)% 了。

    下面是内存结构示意图:

    双向链表结点的简单定义法:class Node {
        public int key, val;
        public Node next, prev;
        public Node(int k, int v) {
            this.key = k;
            this.val = v;
        }
    }
    双向列表结点一般定义法:
    class
    Node<K,V>{ private K key; private V value; private Node<K,V> prev; private Node<K,V> next; }
    然后依靠我们的 Node 类型构建一个双链表,实现几个要用到的 API,这些操作的时间复杂度均为 O(1) :
    class DoubleList {  
        // 在链表头部添加节点 x
        public void addFirst(Node x);
    
        // 删除链表中的 x 节点(x 一定存在)
        public void remove(Node x);
    
        // 删除链表中最后一个节点,并返回该节点
        public Node removeLast();
    
        // 返回链表长度
        public int size();
    }

    这就是普通双向链表的实现,为了让读者集中精力理解 LRU 算法的逻辑,就省略链表的具体代码。

    到这里就能回答刚才“为什么必须要用双向链表”的问题了,因为我们需要删除操作。删除一个链表节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。

    有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚:

     如果能够看懂上述逻辑,翻译成代码就很容易理解了:

    这里就能回答之前的问题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码:

    if (cap == cache.size()) {
        // 删除链表最后一个数据
        Node last = cache.removeLast();
        map.remove(last.key);
    }

    当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。

    至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。

  • 相关阅读:
    了解JVM原理
    封装JS
    “==”和Equals的区别
    SpringMVC请求RequestMapping() 请求乱码
    博客25周
    博客24周
    博客23周
    博客22周
    博客第21周
    博客第21周
  • 原文地址:https://www.cnblogs.com/shijianchuzhenzhi/p/10529125.html
Copyright © 2020-2023  润新知