redis键过期策略、判定原理与算法实现

redis键的过期策略与判定原理

为key设置过期时间

可以在set一个key时设置过期时间,语法:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL],有关过期时间的两个选项如下:

  • EX seconds:设置键key的过期时间,单位为秒。
  • PX milliseconds:设置键key的过期时间,单位为毫秒。
127.0.0.1:6379> set name morris ex 5
OK

可以使用expire命令单独为key设置过期时间,语法:EXPIRE key seconds

127.0.0.1:6379> expire name 5
(integer) 1

可以使用ttl查看key的过期时间,单位为秒,pttl查看key的过期时间,单位为毫秒:

127.0.0.1:6379> set name morris ex 10
OK
127.0.0.1:6379> ttl name
(integer) 8
127.0.0.1:6379> pttl name
(integer) 5353
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> set name morris
OK
127.0.0.1:6379> ttl name
(integer) -1

对于一个过期的key或者不存在的key,ttl返回的是-2,对于正常的key,ttl返回-1,对于一个设置了过期时间的key,ttl返回的是剩余多少秒过期。

可以使用persist命令去除超时时间限制,使其变成一个永久的key:

127.0.0.1:6379> set name morris ex 10
OK
127.0.0.1:6379> persist name
(integer) 1
127.0.0.1:6379> ttl name
(integer) -1

注意点:

  • 如果key存在时,使用set命令会覆盖过期时间,也就是说当成一个新的key来设置。
  • 如果key被rename命令修改,超时时间会转移到新key上面。
127.0.0.1:6379> set name morris ex 30
OK
127.0.0.1:6379> set name tom
OK
127.0.0.1:6379> ttl name
(integer) -1
127.0.0.1:6379> set name morris ex 30
OK
127.0.0.1:6379> rename name myname
OK
127.0.0.1:6379> ttl myname
(integer) 22

如何淘汰过期的keys

Redis中key过期后,有两种淘汰key的方式:被动方式和主动方式。

主动方式

当key超过过期时间后,并不会立即删除key,只有对过期的key执行del、set、getset时才会清除,也就意味着所有对改变key的值的操作都会触发删除动作,当客户端尝试访问它时,key会被发现并主动的过期。

被动方式

光靠主动方式是不够的,因为有些过期的keys,永远不会访问他们,那就永远不会过期,所以redis提供了被动方式,被动方式会定时检测过期的key,然后删除,具体操作如下:

  1. 每隔100ms随机抽取20个key进行过期检测。
  2. 删除20个key中所有已经过期的key。
  3. 如果过期key的比例大于25%,重复步骤1。

被动方式采用一种概率算法,对key进行随机抽样,这样就意味着,在任何给定的时刻,最多会清除1/4的过期key。

配置最大内存

可以在/etc/redis/6379.conf配置文件中限制redis的内存大小:

maxmemory <bytes>

设置maxmemory为0代表没有内存限制。

回收策略

当指定的内存限制大小达到时,需要选择不同的行为,也就是策略来回收一些旧的数据来使得添加数据时可以避免内存限制。

可以通过maxmemory-policy参数来配置具体的回收策略,支持的回收策略如下:

  • noeviction:不回收,对写命令直接返回错误,还是可以进行读操作,如果使用redis作为数据库就要使用这种回收策略,默认使用此策略。
  • volatile-lru:在带有过期时间的key中,尝试回收最近最少使用的key。
  • allkeys-lru:在所有的key中,尝试回收最近最少使用的key。
  • volatile-lfu:在带有过期时间的key中,尝试回收最不经常使用的key。
  • allkeys-lfu:在所有的key中,尝试回收最不经常使用的key。
  • allkeys-random: 在所有的key中,随机回收。
  • volatile-random: 在带有过期时间的key中,随机回收。
  • volatile-ttl: 在带有过期时间的key中,优先回收存活时间(TTL)最短的键。

如果没有满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction差不多了。

近似LRU算法

Redis中的LRU算法并非完整的实现,这意味着Redis并没办法选择最久未被访问的键来进行回收,因为使用真正的LRU算法需要扫描所有的key,这将浪费大量的时间,这与redis高性能的设计初衷相违背。

相反Redis中使用了一个近似LRU的算法,通过对少量key进行抽样,然后从中选择最久未被访问的键来进行回收。Redis提供了下面的参数来调整每次回收时检查的采样数量,以实现调整算法的精度:

maxmemory-samples 5

算法实现

LRU

LRU(The Least Recently Used,最近最少使用算法):如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有被访问的数据最先被淘汰。

实现:可以用双向链表(LinkedList)+哈希表(HashMap)实现(链表用来表示位置,哈希表用来存储和查找)。

package com.morris.redis.demo.cache;

import java.util.HashMap;
import java.util.LinkedList;

public class LRUCache<K, V> {
     

    private int capacity;

    private int size;

    private LinkedList<K> linkedList = new LinkedList<>();

    private HashMap<K, V> hashMap = new HashMap();

    public LRUCache(int capacity) {
     
        this.capacity = capacity;
    }

    public void set(K k, V v) {
     

        if(hashMap.containsKey(k)) {
      // key存在
            linkedList.remove(k); // 从双向链表中移除
            linkedList.addFirst(k); // 插入到双向链表尾部
            hashMap.put(k, v);
            return;
        }

        // key不存在
        if(size == capacity) {
     
            linkedList.removeLast();
            size--;
        }

        linkedList.addFirst(k);
        hashMap.put(k, v);
        size++;
    }

    public V get(K k) {
     
        if(hashMap.containsKey(k)) {
     
            linkedList.remove(k); // 从双向链表中移除
            linkedList.addFirst(k); // 插入到双向链表尾部
            return hashMap.get(k);
        }
        return null;
    }
}

在Java可以直接使用LinkedHashMap来实现:

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache2 extends LinkedHashMap {

    private int capacity;

    public LRUCache2(int capacity) {
        super(16, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }

}

LFU

LFU(Least Frequently Used ,最近最不常用算法):如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最不常用的数据最先被淘汰。

package com.morris.redis.demo.cache.lfu;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;

public class LFUCache<K, V> {
     

    private int capacity;

    private int size;

    public LFUCache(int capacity) {
     
        this.capacity = capacity;
    }

    private HashMap<K, Node<K, V>> hashMap = new HashMap<>();

    private LinkedList<Node<K, V>> linkedList = new LinkedList<>();

    public void set(K k, V v) {
     

        if(hashMap.containsKey(k)) {
      // 存在则更新
            Node<K, V> node = hashMap.get(k);
            node.count = 0; // 增加使用次数
            node.lastTime = System.nanoTime(); // 更新使用时间
            return;
        }

        // 不存在
        if(size == capacity) {
     

            // 删除最近最不常用的key
            Collections.sort(linkedList, (k1, k2) -> {
     
                // 先比较使用次数
                if(k1.count > k2.count) {
     
                    return 1;
                }

                if(k1.count < k2.count) {
     
                    return -1;
                }

                // 再比较最后一次使用时间
                if(k1.lastTime > k2.lastTime) {
     
                    return 1;
                }

                if(k1.lastTime < k2.lastTime) {
     
                    return -1;
                }

                return 0;
            });

            linkedList.removeFirst();
            size--;
        }

        Node<K, V> node = new Node<>(k, v, System.nanoTime());
        hashMap.put(k, node);
        linkedList.addLast(node);
        this.size++;
    }

    public V get(K k) {
     
        V v = null;
        if(hashMap.containsKey(k)) {
     
            Node<K, V> node = hashMap.get(k);
            node.count++; // 增加使用次数
            node.lastTime = System.nanoTime(); // 更新使用时间
            v = node.v;
        }
        return v;
    }

    public void print() {
     
        System.out.println(linkedList);
    }

    private static class Node<K, V> {
     
        K k;
        V v;
        int count; // 使用次数
        long lastTime; // 最后一次使用时间

        public Node(K k, V v, long lastTime) {
     
            this.k = k;
            this.v = v;
            this.lastTime = lastTime;
        }

        @Override
        public String toString() {
     
            final StringBuilder sb = new StringBuilder("Node{");
            sb.append("k=").append(k);
            sb.append(", count=").append(count);
            sb.append(", lastTime=").append(lastTime);
            sb.append('}');
            return sb.toString();
        }
    }
}

使用LinkedList需要对所有的key进行全排序,时间复杂度为O(n)。

考虑到LFU会淘汰访问频率最小的数据,我们需要一种合适的方法按大小顺序维护数据访问的频率。LFU算法本质上可以看做是一个top K问题(K=1),即选出频率最小的元素,因此我们很容易想到可以用二叉堆来选择频率最小的元素,这样的实现比较高效,最终实现策略为小顶堆+哈希表。

使用二叉堆找出所有的key中最小的,时间复杂度为O(logn)。

package com.morris.redis.demo.cache.lfu;

import java.util.HashMap;
import java.util.PriorityQueue;

public class LFUCache2<K, V> {
     

    private int capacity;

    private int size;

    public LFUCache2(int capacity) {
     
        this.capacity = capacity;
    }

    private HashMap<K, Node<K, V>> hashMap = new HashMap<>();

    private PriorityQueue<Node<K, V>> priorityQueue = new PriorityQueue<>((k1, k2) -> {
     
        // 先比较使用次数
        if(k1.count > k2.count) {
     
            return 1;
        }

        if(k1.count < k2.count) {
     
            return -1;
        }

        // 再比较最后一次使用时间
        if(k1.lastTime > k2.lastTime) {
     
            return 1;
        }

        if(k1.lastTime < k2.lastTime) {
     
            return -1;
        }

        return 0;
    });

    public void set(K k, V v) {
     

        if(hashMap.containsKey(k)) {
      // 存在则更新
            Node<K, V> node = hashMap.get(k);
            node.count = 0; // 增加使用次数
            node.lastTime = System.nanoTime(); // 更新使用时间
            return;
        }

        // 不存在
        if(size == capacity) {
     

            // 删除最近最不常用的key
            priorityQueue.remove();
            size--;
        }

        Node<K, V> node = new Node<>(k, v, System.nanoTime());
        hashMap.put(k, node);
        priorityQueue.add(node);
        this.size++;
    }

    public V get(K k) {
     
        V v = null;
        if(hashMap.containsKey(k)) {
     
            Node<K, V> node = hashMap.get(k);
            node.count++; // 增加使用次数
            node.lastTime = System.nanoTime(); // 更新使用时间
            v = node.v;
        }
        return v;
    }

    public void print() {
     
        System.out.println(priorityQueue);
    }

    private static class Node<K, V> {
     
        K k;
        V v;
        int count; // 使用次数
        long lastTime; // 最后一次使用时间

        public Node(K k, V v, long lastTime) {
     
            this.k = k;
            this.v = v;
            this.lastTime = lastTime;
        }

        @Override
        public String toString() {
     
            final StringBuilder sb = new StringBuilder("Node{");
            sb.append("k=").append(k);
            sb.append(", count=").append(count);
            sb.append(", lastTime=").append(lastTime);
            sb.append('}');
            return sb.toString();
        }
    }
}

你可能感兴趣的:(redis,redis,过期策略,LRU,LFU,缓存过期)