一、关于Redis内存回收

Redis是基于内存操作的非关系型数据库,Redis中提供了多种内存回收策略,当内存容量不足时,为了保证程序的运行,这时就不得不淘汰内存中的一些对象,释放这些对象占用的空间,那么选择淘汰哪些对象呢?

Redis的内存回收,主要围绕以下两种方式:

  1. Redis过期策略:删除已经过期的数据;
  2. Redis淘汰策略:内存使用到达maxmemory上限时触发内存淘汰数据。 注意:过期策略和淘汰策略是两种不同的概念,下面分开讲解。

二、Redis过期策略

在Redis中,提供了expire命令设置一个键的过期时间,到期之后Redis会自动删除它,这个在我们的实际使用过程中用的非常多。

Redis中设置过期时间有如下两种方式:

  1. expire命令:expire key seconds(先set key,然后设置过期时间。其中seconds 参数表示键的过期时间,单位为秒。expire 返回值为1表示设置成功,0表示设置失败或者键不存在)
  2. setex命令:setex key seconds value(设置键的同时,直接设置过期时间)

expire命令的seconds单位为秒,最小精确至1秒,如果想要更精确的控制键的过期时间,可以使用pexpire命令,pexpire命令的单位是毫秒。pexpire key 1000与expire key 1相等。

三种过期策略:

1.定时删除: 在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。

  • 优点:保证内存被尽快释放
  • 缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key。

2.惰性删除: key过期的时候不删除, 每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。

  • 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)。
  • 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)。

3.定期删除: 每隔一段时间执行一次删除过期key操作。

  • 优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用–处理"定时删除"的缺点,定期删除过期key–处理"惰性删除"的缺点。
  • 缺点:在内存友好方面,不如"定时删除",在CPU时间友好方面,不如"惰性删除"。
  • 难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)。

Redis采用的过期策略: 惰性删除+定期删除。

三、Redis淘汰策略:

  1. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,使用LRU算法,移除最近最少使用的key;
  2. volatile-lfu:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,使用LFU算法,移除最近最少使用的key;
  3. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key;
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,使用LRU算法,移除最近最少使用的key;
  5. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,使用LFU算法,移除最近最少使用的key;
  6. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key;
  7. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除;
  8. noeviction(默认):当内存不足以容纳新写入数据时,新写入操作会报错。

四、手写LRU

class LRUCache {
    class DLinkedList{
        public int key;
        public int val;
        DLinkedList prev;
        DLinkedList next;
        // 无参构造
        public DLinkedList(){}
        // 有参构造
        public DLinkedList(int key,int val){
            this.key = key;
            this.val = val;
        }
    }
    public int size = 0;
    public int capacity = 0;
    HashMap<Integer,DLinkedList> map = new HashMap<>();
    DLinkedList head = new DLinkedList();
    DLinkedList tail = new DLinkedList();
    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }
    public int get(int key) {
        DLinkedList node = map.get(key);
        if(node != null){
            moveToHead(node);
            return node.val;
        }else{
            return -1;
        }
    }
    public void put(int key, int value) {
        DLinkedList node = map.get(key);
        if(node == null){
            DLinkedList newNode = new DLinkedList(key,value);
            map.put(key,newNode);
            addToHead(newNode);
            size++;
            if(size > capacity){
                // 删除最后一个节点
                // 获取尾部节点
                DLinkedList tail = tailNode();
                // 从map中删除
                map.remove(tail.key);
                // 从链表中删除
                removeNode(tail);
                size--;
            }
        }else{
            node.val = value;
            moveToHead(node);
        }
    }
    // 添加节点到头部
    public void addToHead(DLinkedList node){
        node.prev = head;
        node.next = head.next;
        head.next = node;
        node.next.prev = node;
    }
    // 删除某个节点
    public void removeNode(DLinkedList node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    // 将节点移动到头部
    public void moveToHead(DLinkedList node){
        // 先删除节点
        removeNode(node);
        // 添加到头部
        addToHead(node);
    }
    // 获取尾部节点
    public DLinkedList tailNode(){
        return tail.prev;
    }
}