可以在set一个key时设置过期时间,语法:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
,有关过期时间的两个选项如下:
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
注意点:
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
Redis中key过期后,有两种淘汰key的方式:被动方式和主动方式。
当key超过过期时间后,并不会立即删除key,只有对过期的key执行del、set、getset时才会清除,也就意味着所有对改变key的值的操作都会触发删除动作,当客户端尝试访问它时,key会被发现并主动的过期。
光靠主动方式是不够的,因为有些过期的keys,永远不会访问他们,那就永远不会过期,所以redis提供了被动方式,被动方式会定时检测过期的key,然后删除,具体操作如下:
被动方式采用一种概率算法,对key进行随机抽样,这样就意味着,在任何给定的时刻,最多会清除1/4的过期key。
可以在/etc/redis/6379.conf
配置文件中限制redis的内存大小:
maxmemory <bytes>
设置maxmemory为0代表没有内存限制。
当指定的内存限制大小达到时,需要选择不同的行为,也就是策略来回收一些旧的数据来使得添加数据时可以避免内存限制。
可以通过maxmemory-policy
参数来配置具体的回收策略,支持的回收策略如下:
如果没有满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction差不多了。
Redis中的LRU算法并非完整的实现,这意味着Redis并没办法选择最久未被访问的键来进行回收,因为使用真正的LRU算法需要扫描所有的key,这将浪费大量的时间,这与redis高性能的设计初衷相违背。
相反Redis中使用了一个近似LRU的算法,通过对少量key进行抽样,然后从中选择最久未被访问的键来进行回收。Redis提供了下面的参数来调整每次回收时检查的采样数量,以实现调整算法的精度:
maxmemory-samples 5
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(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();
}
}
}