一、关于Redis内存回收
Redis是基于内存操作的非关系型数据库,Redis中提供了多种内存回收策略,当内存容量不足时,为了保证程序的运行,这时就不得不淘汰内存中的一些对象,释放这些对象占用的空间,那么选择淘汰哪些对象呢?
Redis的内存回收,主要围绕以下两种方式:
- Redis过期策略:删除已经过期的数据;
- Redis淘汰策略:内存使用到达maxmemory上限时触发内存淘汰数据。 注意:过期策略和淘汰策略是两种不同的概念,下面分开讲解。
二、Redis过期策略
在Redis中,提供了expire命令设置一个键的过期时间,到期之后Redis会自动删除它,这个在我们的实际使用过程中用的非常多。
Redis中设置过期时间有如下两种方式:
- expire命令:expire key seconds(先set key,然后设置过期时间。其中seconds 参数表示键的过期时间,单位为秒。expire 返回值为1表示设置成功,0表示设置失败或者键不存在)
- 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淘汰策略:
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,使用LRU算法,移除最近最少使用的key;
- volatile-lfu:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,使用LFU算法,移除最近最少使用的key;
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key;
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,使用LRU算法,移除最近最少使用的key;
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,使用LFU算法,移除最近最少使用的key;
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key;
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除;
- 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;
}
}