>>号外:关注“Java精选”公众号,回复“面试资料”,免费领取资料!“Java精选面试题”小程序,3000+ 道面试题在线刷,最新、最全 Java 面试题!

很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现LRU。

我的第一反应是操作系统课程里学过,应该是内存不够的场景下,淘汰旧内容的策略。LRU ... Least Recent Used,淘汰掉最不经常使用的。

可以稍微多补充两句,因为计算机体系结构中,最大的最可靠的存储是硬盘,它容量很大,并且内容可以固化,但是访问速度很慢,所以需要把使用的内容载入内存中;内存速度很快,但是容量有限,并且断电后内容会丢失,并且为了进一步提升性能,还有CPU内部的 L1 Cache,L2 Cache等概念。

因为速度越快的地方,它的单位成本越高,容量越小,新的内容不断被载入,旧的内容肯定要被淘汰,所以就有这样的使用背景。

LRU原理

在一般标准的操作系统教材里,会用下面的方式来演示 LRU 原理,假设内存只能容纳3个页大小,按照 7 0 1 2 0 3 0 4 的次序访问页。假设内存按照栈的方式来描述访问时间,在上面的,是最近访问的,在下面的是,最远时间访问的,LRU就是这样工作的。

但是如果让我们自己设计一个基于 LRU 的缓存,这样设计可能问题很多,这段内存按照访问时间进行了排序,会有大量的内存拷贝操作,所以性能肯定是不能接受的。

那么如何设计一个LRU缓存,使得放入和移除都是 O(1) 的,我们需要把访问次序维护起来,但是不能通过内存中的真实排序来反应,有一种方案就是使用双向链表。

基于 HashMap 和 双向链表实现 LRU 的

整体的设计思路是,可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。

LRU 存储是基于双向链表实现的,下面的图演示了它的原理。其中 h 代表双向链表的表头,t 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。“Java精选面试题”小程序,3000+ 道面试题在线刷,最新、最全 Java 面试题。

下面展示了,预设大小是 3 的,LRU存储的在存储和访问过程中的变化。为了简化图复杂度,图中没有展示 HashMap部分的变化,仅仅演示了上图 LRU 双向链表的变化。我们对这个LRU缓存的操作序列如下:

save("key1", 7)save("key2", 0)save("key3", 1)save("key4", 2)get("key2")save("key5", 3)get("key2")save("key6", 4)

相应的 LRU 双向链表部分变化如下:

总结一下核心操作的步骤:

  • save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。“Java精选面试题”小程序,3000+ 道面试题在线刷,最新、最全 Java 面试题。

  • get(key),通过 HashMap 找到 LRU 链表节点,把节点插入到队头,返回缓存的值。

完整基于 Java 的代码参考如下

class DLinkedNode {String key;int value;DLinkedNode pre;DLinkedNode post;
}

LRU Cache

public class LRUCache {private Hashtable<Integer, DLinkedNode>cache = new Hashtable<Integer, DLinkedNode>();private int count;private int capacity;private DLinkedNode head, tail;public LRUCache(int capacity) {this.count = 0;this.capacity = capacity;head = new DLinkedNode();head.pre = null;tail = new DLinkedNode();tail.post = null;head.post = tail;tail.pre = head;}public int get(String key) {DLinkedNode node = cache.get(key);if(node == null){return -1; // should raise exception here.}// move the accessed node to the head;this.moveToHead(node);return node.value;}public void set(String key, int value) {DLinkedNode node = cache.get(key);if(node == null){DLinkedNode newNode = new DLinkedNode();newNode.key = key;newNode.value = value;this.cache.put(key, newNode);this.addNode(newNode);++count;if(count > capacity){// pop the tailDLinkedNode tail = this.popTail();this.cache.remove(tail.key);--count;}}else{// update the value.node.value = value;this.moveToHead(node);}}/*** Always add the new node right after head;*/private void addNode(DLinkedNode node){node.pre = head;node.post = head.post;head.post.pre = node;head.post = node;}/*** Remove an existing node from the linked list.*/private void removeNode(DLinkedNode node){DLinkedNode pre = node.pre;DLinkedNode post = node.post;pre.post = post;post.pre = pre;}/*** Move certain node in between to the head.*/private void moveToHead(DLinkedNode node){this.removeNode(node);this.addNode(node);}// pop the current tail.private DLinkedNode popTail(){DLinkedNode res = tail.pre;this.removeNode(res);return res;}
}

那么问题的后半部分,是 Redis 如何实现,这个问题这么问肯定是有坑的,那就是redis肯定不是这样实现的。

Redis的LRU实现

如果按照HashMap和双向链表实现,需要额外的存储存放 next 和 prev 指针,牺牲比较大的存储空间,显然是不划算的。所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的,具体分析如下:

为了支持LRU,Redis 2.8.19中使用了一个全局的LRU时钟,server.lruclock,定义如下,

#define REDIS_LRU_BITS 24
unsigned lruclock:REDIS_LRU_BITS; /* Clock for LRU eviction */

默认的LRU时钟的分辨率是1秒,可以通过改变REDIS_LRU_CLOCK_RESOLUTION宏的值来改变,Redis会在serverCron()中调用updateLRUClock定期的更新LRU时钟,更新的频率和hz参数有关,默认为100ms一次,如下,

#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */void updateLRUClock(void) {server.lruclock = (server.unixtime / REDIS_LRU_CLOCK_RESOLUTION) &REDIS_LRU_CLOCK_MAX;
}

server.unixtime是系统当前的unix时间戳,当 lruclock 的值超出REDIS_LRU_CLOCK_MAX时,会从头开始计算,所以在计算一个key的最长没有访问时间时,可能key本身保存的lru访问时间会比当前的lrulock还要大,这个时候需要计算额外时间,如下,

/* Given an object returns the min number of seconds the object was never* requested, using an approximated LRU algorithm. */
unsigned long estimateObjectIdleTime(robj *o) {if (server.lruclock >= o->lru) {return (server.lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;} else {return ((REDIS_LRU_CLOCK_MAX - o->lru) + server.lruclock) *REDIS_LRU_CLOCK_RESOLUTION;}
}

Redis支持和LRU相关淘汰策略包括,

  • volatile-lru 设置了过期时间的key参与近似的lru淘汰策略

  • allkeys-lru 所有的key均参与近似的lru淘汰策略

当进行LRU淘汰时,Redis按如下方式进行的,

....../* volatile-lru and allkeys-lru policy */else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU){for (k = 0; k < server.maxmemory_samples; k++) {sds thiskey;long thisval;robj *o;de = dictGetRandomKey(dict);thiskey = dictGetKey(de);/* When policy is volatile-lru we need an additional lookup* to locate the real key, as dict is set to db->expires. */if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)de = dictFind(db->dict, thiskey);o = dictGetVal(de);thisval = estimateObjectIdleTime(o);/* Higher idle time is better candidate for deletion */if (bestkey == NULL || thisval > bestval) {bestkey = thiskey;bestval = thisval;}}}......

Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高,对性能有一定影响,样本值默认为5。

总结

看来,虽然一个简单的概念,在工业界的产品中,为了追求空间的利用率,也会采用权衡的实现方案。

作者: 文西

blog.csdn.net/hopeztm/article/details/79547052

往期精选  点击标题可跳转

【029期】面试官:数据量很大的情况下,对于分页查询你有什么优化方案吗?

【030期】面试官问:MySQL发生死锁有哪些原因,怎么避免?

【031期】面试官问:为什么 StringBuilder 线程不是安全的?

【032期】2021年 Java 面试中 Linux 最高频的五个基本面试题

【033期】面试官问:说一说 Spring 中接口 bean 是如何注入的吗?

【034期】美团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

【035期】面试官问:什么是耦合?解耦合的方法有哪几种?

【036期】面试官问:公司项目中 Java 多线程一般适用于什么场景?

【037期】面试官:Spring Boot 项目中如何处理重复请求和并发请求问题?

【038期】面试官问:说一说项目中单点登录的实现原理?

点个赞,就知道你“在看”!

【039期】头条面试:说一说 LRU 原理和 Redis 如何实现?相关推荐

  1. 今日头条的面试题:LRU原理和Redis实现

    很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现 LRU. 我的第一反应是操作系统课程里学过,应该是内存不够的场景下,淘汰旧内容的策略.LRU ...

  2. LRU原理和Redis实现——一个今日头条的面试题

    很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现 LRU. 我的第一反应是操作系统课程里学过,应该是内存不够的场景下,淘汰旧内容的策略.LRU ...

  3. 分享一个今日头条的面试题——LRU原理和Redis实现

    很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现 LRU. 我的第一反应该是内存不够的场景下,淘汰旧内容的策略.LRU ... Least Re ...

  4. LRU原理和Redis实现——一个今日头条的面试题(转载)

    很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现 LRU. 我的第一反应是操作系统课程里学过,应该是内存不够的场景下,淘汰旧内容的策略.LRU ...

  5. 【227期】面试官:MySQL 数据查询太多会 OOM 吗?

    点击上方"Java精选",选择"设为星标" 别问别人为什么,多问自己凭什么! 下方有惊喜,留言必回,有问必答! 每天 08:15 更新文章,每天进步一点点... ...

  6. 从简历被拒,到拿下头条面试,我花了一年的时间(经验分享+面试题)

    作者:无精疯 原文链接:https://www.zhihu.com/people/wu-jin-feng-41 前言 我是二本院校非科班的,从简历被拒,到拿下头条面试,我花了一年的时间. 一.印象中的 ...

  7. 51CTO下载中心往期头条回顾(2)

    2012.02.17 <Linux运维趋势>2012年2月号:运维安全准则 [运维人员必杀技:网络安全工具百宝箱] [webOS嵌入式开发入门] [Linux/Unix Shell编程入门 ...

  8. ssm中怎么使tomcat一起动就执行一个controller_【200期】面试官:你能简单说说 SpringMVC 的执行原理吗?...

    点击上方"Java面试题精选",关注公众号 面试刷图,查缺补漏 >>号外:往期面试题,10篇为一个单位归置到本公众号菜单栏->面试题,有需要的欢迎翻阅 阶段汇总集 ...

  9. 只导表前10条数据_【205期】面试官:数据量很大的情况下,对于分页查询你有什么优化方案吗?...

    点击上方"Java面试题精选",关注公众号 面试刷图,查缺补漏 >>号外:公众号改版后文章顺序不固定,欢迎大家把我们面试题精选这个公众号设置为星标,感谢大家一年的支持! ...

最新文章

  1. What you should know about .so files
  2. c语言调用shell命令一 popen使用以及获取命令返回值
  3. 关于linux技术的的外文,关于Linux的介绍-外文翻译.doc
  4. jzoj5353-村通网【最小生成树】
  5. python关机linux_Python学习第157课——Linux切换用户、关机、查看正在运行的程序...
  6. 【CodeForces - 270C】Magical Boxes (思维,进制,有坑)
  7. vscode怎样导入数据_【Python开发】用VSCode+Jupyter notebook 编写 Python
  8. 中国大学MOOC-数据结构基础习题集、06-3、公路村村通
  9. 程序员如何跨过自我推销的难关?
  10. 网银支付_【成果巡礼】企业网银支付功能上线 上海全市法院再添便民新举措...
  11. Spring中父子容器的实现实例
  12. jq常用过滤器_JQuery的常用选择器、过滤器、方法全面介绍
  13. SpringBoot项目如何部署到独立的tomcat上
  14. mysql5.5java安装_配置非安装版的mysql 5.5
  15. ElasticSearch开发问题汇总(不断更新中)
  16. 怎样音频转文字?用这些音频转文字免费版就可以啦
  17. 等差数列及等比数列求和公式
  18. 数据挖掘 --如何有效地进行数据挖掘和分析
  19. KeyError: ‘Worksheet sheet does not exist.‘
  20. 【备忘】Mongodb/Redis/HBase NoSql视频教程2017数据库自学教程

热门文章

  1. 目标检测之Two Stage
  2. RGW Bucket Shard优化
  3. 这个城市最安静的声音 - Suzy
  4. 简述直方图和柱形图的区别_什么是直方图?跟柱状图有什么区别?终于有人讲明白了...
  5. 深入理解ActiveMQ支持的2类消息发送接收模型queue和topic
  6. Windows 10 Edt LTSC中文系统安装教程
  7. 基于Java的项目人力资源管理系统【附:源码课件】
  8. 基于 CNT 的射频辐射热计开发研究的 CPX-VF 低温探针台
  9. c语言中怎么画直线,ps如何画直线 【操作流程】
  10. pytorch读取常用数据集dataset实现例子