很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 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)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。

下面展示了,预设大小是 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 双向链表部分变化如下:

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

  1. save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
  2. get(key),通过 HashMap 找到 LRU 链表节点,把节点插入到队头,返回缓存的值。

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

  1. class DLinkedNode {
  2. String key;
  3. int value;
  4. DLinkedNode pre;
  5. DLinkedNode post;
  6. }

LRU Cache

  1. public class LRUCache {
  2. private Hashtable<Integer, DLinkedNode>
  3. cache = new Hashtable<Integer, DLinkedNode>();
  4. private int count;
  5. private int capacity;
  6. private DLinkedNode head, tail;
  7. public LRUCache(int capacity) {
  8. this.count = 0;
  9. this.capacity = capacity;
  10. head = new DLinkedNode();
  11. head.pre = null;
  12. tail = new DLinkedNode();
  13. tail.post = null;
  14. head.post = tail;
  15. tail.pre = head;
  16. }
  17. public int get(String key) {
  18. DLinkedNode node = cache.get(key);
  19. if(node == null){
  20. return -1; // should raise exception here.
  21. }
  22. // move the accessed node to the head;
  23. this.moveToHead(node);
  24. return node.value;
  25. }
  26. public void set(String key, int value) {
  27. DLinkedNode node = cache.get(key);
  28. if(node == null){
  29. DLinkedNode newNode = new DLinkedNode();
  30. newNode.key = key;
  31. newNode.value = value;
  32. this.cache.put(key, newNode);
  33. this.addNode(newNode);
  34. ++count;
  35. if(count > capacity){
  36. // pop the tail
  37. DLinkedNode tail = this.popTail();
  38. this.cache.remove(tail.key);
  39. --count;
  40. }
  41. }else{
  42. // update the value.
  43. node.value = value;
  44. this.moveToHead(node);
  45. }
  46. }
  47. /**
  48. * Always add the new node right after head;
  49. */
  50. private void addNode(DLinkedNode node){
  51. node.pre = head;
  52. node.post = head.post;
  53. head.post.pre = node;
  54. head.post = node;
  55. }
  56. /**
  57. * Remove an existing node from the linked list.
  58. */
  59. private void removeNode(DLinkedNode node){
  60. DLinkedNode pre = node.pre;
  61. DLinkedNode post = node.post;
  62. pre.post = post;
  63. post.pre = pre;
  64. }
  65. /**
  66. * Move certain node in between to the head.
  67. */
  68. private void moveToHead(DLinkedNode node){
  69. this.removeNode(node);
  70. this.addNode(node);
  71. }
  72. // pop the current tail.
  73. private DLinkedNode popTail(){
  74. DLinkedNode res = tail.pre;
  75. this.removeNode(res);
  76. return res;
  77. }
  78. }

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

Redis的LRU实现

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

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

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

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

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

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

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

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

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

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

  1. ......
  2. /* volatile-lru and allkeys-lru policy */
  3. else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
  4. server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
  5. {
  6. for (k = 0; k < server.maxmemory_samples; k++) {
  7. sds thiskey;
  8. long thisval;
  9. robj *o;
  10. de = dictGetRandomKey(dict);
  11. thiskey = dictGetKey(de);
  12. /* When policy is volatile-lru we need an additional lookup
  13. * to locate the real key, as dict is set to db->expires. */
  14. if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
  15. de = dictFind(db->dict, thiskey);
  16. o = dictGetVal(de);
  17. thisval = estimateObjectIdleTime(o);
  18. /* Higher idle time is better candidate for deletion */
  19. if (bestkey == NULL || thisval > bestval) {
  20. bestkey = thiskey;
  21. bestval = thisval;
  22. }
  23. }
  24. }
  25. ......

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

总结

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

传送门 https://zhuanlan.zhihu.com/p/34133067

原文:https://blog.csdn.net/hopeztm/article/details/79547052

关于linkedhashmap实现LRU:https://www.cnblogs.com/lzrabbit/p/3734850.html

LRU原理和Redis实现——一个今日头条的面试题(转载)相关推荐

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

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

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

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

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

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

  4. 【039期】头条面试:说一说 LRU 原理和 Redis 如何实现?

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

  5. 惊了!原来这就是今日头条的面试题!

    最近收到了一个大兄弟念叨,说他去面了今日头条,一路下来感觉自己还是蛮顺畅的,然后顺带给我大致说了一下今日头条的面试题. 顺便插个话,想要拥有百次面试的磨练吗,想要的小伙伴可以戳这里暗号:CSDN即可拥 ...

  6. 2020年开春最新面试!今日头条 Android 面试题及答案 (已拿到 offer)

    面试时间:2019.12.29 1~3面.2020.1.03 4~6面.2020.1.06 HR面 面试部门 + 岗位:商业化 - 高级 Android 开发工程师 面试感想:整体面得比较累,基础面. ...

  7. python编程100例头条-今日头条python面试题之编程篇

    最大映射 有 n 个字符串,每个字符串都是由 A-J 的大写字符构成.现在你将每个字符映射为一个 0-9 的数字,不同字符映射为不同的数字.这样每个字符串就可以看做一个整数,唯一的要求是这些整数必须是 ...

  8. java 今日头条面试_【面试分享】今日头条Java面试题,复习资料完整版PDF下载

    2021年,字节的技术岗依旧是最香的,而且随着字节的规模不断扩大,机会也越来越多.马上迎来金三银四,很多小伙伴都在撸题备战中. 2021年,字节的技术岗依旧是最香的,而且随着字节的规模不断扩大,机会也 ...

  9. 今日头条的面试题(部分)

    1.给定一个有序数组,输出平方后消重结果中数字的个数,每次删除一个最大的数 比如: -2, -2, -1,0,1消重后有3个数: 0,0,1,2,3,4,5,5,6消重后有7个数 O(n)复杂度解法: ...

最新文章

  1. Web前端经典面试试题(二)
  2. vs linux 交叉编译,VS结合VisualGDB搭建OpenWrt交叉编译远程调试开发环境
  3. 局域网内同时使用两台路由器的配置方法
  4. mysql deadlock found when trying to get lock暴力解决
  5. 字符串函数 replace() 方法妙用
  6. UNITY3D 脑袋顶血顶名
  7. vue用阿里云oss上传图片使用分片上传只能上传100kb以内的解决办法
  8. VB6 如何连接MYSQL数据库
  9. android 贝塞尔曲线 波浪线,Android 贝塞尔曲线实现水纹波动效果
  10. python request 库传送formdata_Python Requests库 form-data 上传文件操作
  11. 类火墙的iptables
  12. (十三)中介者模式详解(玄幻版)
  13. SQL像数组一样处理字符串、分割字符串(标量值函数[用户自定义函数])
  14. 并发性(concurrency)和并行性(parallel)区别
  15. 在Linux上运行procmem和procrank
  16. 在dll中用DirectSound8同时播放多个wav文件不能发声
  17. 内网端口映射工具之80端口映射和全端口映射及辅助发布网站应用
  18. Sagemath | 基于大整数分解的Rabin加密
  19. MATLAB adf检验和kpss检验结果说明
  20. 【Unity】 HTFramework框架(十)Resource资源管理器

热门文章

  1. 手机文档被删除怎么恢复,如何恢复
  2. python 从文件夹读取图片
  3. QuicKer——电脑万能神器
  4. 兄弟们,网络安全培训机构现在哪个好?
  5. 短视频热点实时分享|想上热门你不能不知道的事
  6. 自然语言处理NLP星空智能对话机器人系列:深入理解Transformer自然语言处理 Training a GPT-2 language model
  7. Java构造方法 重载与重写
  8. oracle函数记录-trunc日期格式ww、iw、w的区别
  9. 深入学习js的classList
  10. 3CTF的两道流量分析题