redis面试题,redis的内存淘汰策略以及手写LRU算法

面试题:

  • 生产上你们你们的redis内存设置多少?
  • 如何配置、修改redis的内存大小
  • 如果内存满了你怎么办?
  • redis清理内存的方式?定期删除和惰性删除了解过吗
  • redis缓存淘汰策略
  • redis的LRU了解过吗?可否手写一个LRU算法
1、查看Redis最大占用内存
  • 配置文件redis.conf的maxmemory参数,maxmemory是bytes字节类型,注意转换。
  • 命令获取,config get maxmemory
2、redis默认内存多少可以用?

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存

3、一般生产上你如何配置?

一般推荐Redis设置内存为最大物理内存的四分之三。

4、如何修改redis内存设置?
  • 修改配置文件redis.conf的maxmemory参数,如:maxmemory 104857600
  • 通过命令修改 config set maxmemory 1024。
5、查看redis内存使用情况?

info memory

6、redis打满会有什么现象?

默认报OOM异常, OOM command not allowed when used memory > ‘maxmemory’.

7、如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?

不是,redis有过期键的删除策略。

8、redis过期键的删除策略?
  • 定时删除 - 总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
  • 惰性删除 - 总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
  • 定期删除 - 定期抽样key,判断是否过期(存在漏网之鱼)

定时删除

Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。

立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,这会产生大量的性能消耗,同时也会影响数据的读取操作。

惰性删除

数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据;发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏 – 无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,导致redis内存空间紧张或者很快耗尽。
定期删除

定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询Redis库中的时效性数据,来用随机抽取的策略,利用过期数据占比的方式控制删除频度

特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间(随机抽查,重点抽查)

举例:
redis默认每个100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

上述步骤都过堂了,还有漏洞吗?

定期删除时,从来没有被抽查到
惰性删除时,也从来没有被点中使用过
上述2步骤====>大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

内存淘汰策略登场(Redis 6.0.8版本)

LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

  • noeviction:不会驱逐任何key
  • volatile-ttl:删除马上要过期的key
  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
  • volatile-Iru:对所有设置了过期时间的key使用LRU算法进行删除
  • allkeys-random:对所有key随机删除
  • volatile-random:对所有设置了过期时间的key随机删除
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • allkeys-Iru:对所有key使用LRU算法进行删除

如何修改内存淘汰策略:

  • 命令 设置 config set maxmemory-policy noeviction 查看config get maxmemory-policy
  • 配置文件 - 配置文件redis.conf的maxmemory-policy参数 例如:maxmemory-policy volatile-Iru
9、lru算法的思想

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
设计思想
所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)

特性要求
必须要有顺序之分,一区分最近使用的和很久没有使用的数据排序。
写和读操作一次搞定。
如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)

查找快、插入快、删除快,且还需要先后排序---------->什么样的数据结构可以满足这个问题?
hash+双向链表

LRU的算法核心是哈希链表

本质就是HashMap + DoubleLinkedList

时间复杂度是O(1),哈希表+双向链表的结合体

10、手写Lru算法(第一种,用JDk自带的LinkedHashMap完成lru算法)
import java.util.LinkedHashMap;

public class LRUCache {
	
	private LinkedHashMap<Integer, Integer> cache;
	
    public LRUCache(int capacity) {
        cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true){
        	
			private static final long serialVersionUID = 1L;
			

			@Override
        	protected boolean removeEldestEntry(java.util.Map.Entry<Integer, Integer> eldest) {
        		return size() > capacity;
        	}
        	
        };
    }
    
    public int get(int key) {
    	return cache.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        cache.put(key, value);
    }
    
    @Override
    public String toString() {
    	return cache.toString();
    }
}
11、哈希表 + 双向链表完成LRU
class LRUCache2{
	class Node<K, V>{//双向链表节点
		K key;
		V value;
		Node<K, V> prev;
		Node<K, V> next;
		
		public Node() {
			this.prev = this.next = null;
		}
		public Node(K key, V value) {
			super();
			this.key = key;
			this.value = value;
		}
	}
	
    //新的插入头部,旧的从尾部移除
	class DoublyLinkedList<K, V>{
		Node<K, V> head;
		Node<K, V> tail;
		
		public DoublyLinkedList() {
            //头尾哨兵节点
			this.head = new Node<K, V>();
			this.tail = new Node<K, V>();
			this.head.next = this.tail;
			this.tail.prev = this.head;
		}
		
		public void addHead(Node<K, V> node) {
			node.next = this.head.next;
			node.prev = this.head;
			this.head.next.prev = node;
			this.head.next = node;
		}
		
		public void removeNode(Node<K, V> node) {
			node.prev.next = node.next;
			node.next.prev = node.prev;
			node.prev = null;
			node.next = null;

		}
		
		public Node<K, V> getLast() {
			if(this.tail.prev == this.head)
				return null;
			return this.tail.prev;
		}

	}
	
	private int cacheSize;
	private Map<Integer, Node<Integer, Integer>> map;
	private DoublyLinkedList<Integer, Integer> doublyLinkedList;
	
	
	public LRUCache2(int cacheSize) {
		this.cacheSize = cacheSize;
		map = new HashMap<>();
		doublyLinkedList = new DoublyLinkedList<>();
	}

	public int get(int key) {
		if(!map.containsKey(key)) {
			return -1;
		}
		
		Node<Integer, Integer> node = map.get(key);
        
        //更新节点位置,将节点移置链表头
		doublyLinkedList.removeNode(node);
		doublyLinkedList.addHead(node);
		
		return node.value;
	}
	
	public void put(int key, int value) {
		
		if(map.containsKey(key)) {
			
			Node<Integer, Integer> node = map.get(key);
			node.value = value;
			map.put(key, node);
			
            
			doublyLinkedList.removeNode(node);
			doublyLinkedList.addHead(node);
		}else {
			
			if(map.size() == cacheSize) {//已达到最大容量了,把旧的移除,让新的进来
				Node<Integer, Integer> lastNode = doublyLinkedList.getLast();
				map.remove(lastNode.key);//node.key主要用处,反向连接map
				doublyLinkedList.removeNode(lastNode);
			}
			
			Node<Integer, Integer> newNode = new Node<>(key, value);
			map.put(key, newNode);
			doublyLinkedList.addHead(newNode);
		}
	}	
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值