其实吧,LRU也就那么回事。

这是why哥的第 81 篇原创文章git

你面试的时候碰见过LRU吗?

LRU 算法,全称是Least Recently Used。github

翻译过来就是最近最少使用算法。面试

这个算法的思想就是:若是一个数据在最近一段时间没有被访问到,那么在未来它被访问的可能性也很小。因此,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。redis

听描述你也知道了,它是一种淘汰算法。算法

这个算法也是面试的一个高频考点。sql

有的面试官甚至要求手撸一个 LRU 算法出来。数据库

其实我以为吧,遇到这种状况也不要慌,你就按照本身的思路写一个出来就行。数组

赌一把,面试官也许本身短期内都手撸不出来一个无 bug 的 LRU。他也只是检查几个关键点、看看你的代码风格、观察一下你的解题思路而已。缓存

但其实大多数状况下面试场景都是这样的:数据结构

面试官:你知道 LRU 算法吗?

我:知道,翻译过来就是最近最少使用算法。其思想是(前面说过,就不复述了)......

面试官:那你能给我谈谈你有哪些方法来实现 LRU 算法呢?

这个时候问的是什么?

问的是:咱们都知道这个算法的思路了,请你按照这个思路给出一个能够落地的解决方案。

不用徒手撸一个。

方案一:数组

若是以前彻底没有接触过 LRU 算法,仅仅知道其思路。

第一次听就要求你给一个实现方案,那么数组的方案应该是最容易想到的。

假设咱们有一个定长数组。数组中的元素都有一个标记。这个标记能够是时间戳,也能够是一个自增的数字。

假设咱们用自增的数字。

每放入一个元素,就把数组中已经存在的数据的标记更新一下,进行自增。当数组满了后,就将数字最大的元素删除掉。

每访问一个元素,就将被访问的元素的数字置为 0 。

这不就是 LRU 算法的一个实现方案吗?

按照这个思路,撸一份七七八八的代码出来,问题应该不大吧?

可是这一种方案的弊端也是很明显:须要不停地维护数组中元素的标记。

那么你以为它的时间复杂度是多少?

是的,每次操做都伴随着一次遍历数组修改标记的操做,因此时间复杂度是O(n)。

可是这个方案,面试官确定是不会满意的。由于,这不是他心中的标准答案。

也许他都没想过:你还能给出这种方案呢?

可是它不会说出来,只会轻轻的说一句:还有其余的方案吗?

方案二:链表

因而你扣着脑袋想了想。最近最少使用,感受是须要一个有序的结构。

我每插入一个元素的时候,就追加在数组的末尾。

我每访问一次元素,就把被访问的元素移动到数组的末尾。

这样最近被用的必定是在最后面的,头部的就是最近最少使用的。

当指定长度被用完了以后,就把头部元素移除掉就好了。

这是个什么结构?

这不就是个链表吗?

维护一个有序单链表,越靠近链表头部的结点是越早以前访问的。

当有一个新的数据被访问时,咱们从链表头部开始顺序遍历链表。

若是此数据以前已经被缓存在链表中了,咱们遍历获得这个数据的对应结点,并将其从原来的位置删除,并插入到链表尾部。

若是此数据没在缓存链表中,怎么办?

分两种状况:

  • 若是此时缓存未满,可直接在链表尾部插入新节点存储此数据;
  • 若是此时缓存已满,则删除链表头部节点,再在链表尾部插入新节点。

你看,这不又是 LRU 算法的一个实现方案吗?

按照这个思路,撸一份八九不离十的代码出来,问题应该不大吧?

这个方案比数组的方案好在哪里呢?

我以为就是莫名其妙的高级感,就是看起来就比数组高级了一点。

从时间复杂度的角度看,由于链表插入、查询的时候都要遍历链表,查看数据是否存在,因此它仍是O(n)。

总之,这也不是面试官想要的答案。

当你回答出这个方案以后,面试官也许会说:你能不能给我一个查询和插入的时间复杂度都是O(1)的解决方案?

到这里,就得看天分了。

有一说一,若是我以前彻底没有接触过 LRU 算法,我能够很是自信的说:

方案三:双向链表+哈希表。

若是咱们想要查询和插入的时间复杂度都是O(1),那么咱们须要一个知足下面三个条件的数据结构:

  • 1.首先这个数据结构必须是有时序的,以区分最近使用的和好久没有使用的数据,当容量满了以后,要删除最久未使用的那个元素。
  • 2.要在这个数据结构中快速找到某个 key 是否存在,并返回其对应的 value。
  • 3.每次访问这个数据结构中的某个 key,须要将这个元素变为最近使用的。也就是说,这个数据结构要支持在任意位置快速插入和删除元素。

那么,你说什么样的数据结构同时符合上面的条件呢?

查找快,咱们能想到哈希表。可是哈希表的数据是乱序的。

有序,咱们能想到链表,插入、删除都很快,可是查询慢。

因此,咱们得让哈希表和链表结合一下,成长一下,造成一个新的数据结构,那就是:哈希链表,LinkedHashMap。

这个结构大概长这样:

借助这个结构,咱们再来分析一下上面的三个条件:

  • 1.若是每次默认从链表尾部添加元素,那么显然越靠近尾部的元素就越是最近使用的。越靠近头部的元素就是越久未使用的。
  • 2.对于某一个 key ,能够经过哈希表快速定位到链表中的节点,从而取得对应的 value。
  • 3.链表显然是支持在任意位置快速插入和删除的,修改指针就行。可是单链表没法按照索引快速访问某一个位置的元素,都是须要遍历链表的,因此这里借助哈希表,能够经过 key,快速的映射到任意一个链表节点,而后进行插入和删除。

这才是面试官想要关于 LRU 的正确答案。

可是你觉得回答到这里就结束了吗?

面试官为了确认你的掌握程度,还会追问一下。

那么请问:为何这里要用双链表呢,单链表为何不行?

你内心一慌:我靠,这题我也背过。一时想不起来了。

因此,别只顾着背答案,得理解。

你想啊,咱们是否是涉及到删除元素的操做?

那么链表删除元素除了本身自己的指针信息,还须要什么东西?

是否是还须要前驱节点的指针?

那么咱们这里要求时间复杂度是O(1),因此怎么才能直接获取到前驱节点的指针?

这玩意是否是就得上双链表?

咦,你看在一波灵魂追问中,就获得了答案。

面试官的第二个问题又随之而来了:哈希表里面已经保存了 key ,那么链表中为何还要存储 key 和 value 呢,只存入 value 不就好了?

不会也不要慌,你先分析一波。

刚刚咱们说删除链表中的节点,须要借助双链表来实现O(1)。

删除了链表中的节点,而后呢?

是否是还忘记了什么东西?

是否是还有一个哈希表忘记操做了?

哈希表是否是也得进行对应的删除操做?

删除哈希表须要什么东西?

是否是须要 key,才能删除对应的 value?

这个 key 从哪里来?

是否是只能从链表中的结点里面来?

若是链表中的结点,只有 value 没有 key,那么咱们就没法删除哈希表的 key。那不就完犊子了吗?

又是一波灵魂追问。

因此,你如今知道答案了吗?

另外在多说一句,有的小伙伴可能会直接回答借助 LinkedHashMap 来实现。

我以为吧,你要是实在不知道,也能够这样说。

可是,这个回答多是面试官最不想听到的回答了。

他会以为你投机取巧。

可是呢,实际开发中,真正要用的时候,咱们仍是用的 LinkedHashMap。

你说这个事情,难受不难受。

好了,你觉得到这里面试就结束了?

LRU 在 MySQL 中的应用

面试官:小伙子刚刚 LRU 回答的不错哈。要不你给我讲讲,LRU 在 MySQL 中的应用?

LRU 在 MySQL 的应用就是 Buffer Pool,也就是缓冲池。

它的目的是为了减小磁盘 IO。

缓冲池具体是干啥的,我这里就不展开说了。

你就知道它是一块连续的内存,默认大小 128M,能够进行修改。

这一块连续的内存,被划分为若干默认大小为 16KB 的页。

既然它是一个 pool,那么必然有满了的时候,怎么办?

就得移除某些页了,对吧?

那么问题就来了:移除哪些页呢?

刚刚说了,它是为了减小磁盘 IO。因此应该淘汰掉好久没有被访问过的页。

好久没有使用,这不就是 LRU 的主场吗?

可是在 MySQL 里面并非简单的使用了 LRU 算法。

由于 MySQL 里面有一个预读功能。预读的出发点是好的,可是有可能预读到并不须要被使用的页。

这些页也被放到了链表的头部,容量不够,致使尾部元素被淘汰。

哦豁,下降命中率了,凉凉。

还有一个场景是全表扫描的 sql,有可能直接把整个缓冲池里面的缓冲页都换了一遍,影响其余查询语句在缓冲池的命中率。

那么怎么处理这种场景呢?

把 LRU 链表分为两截,一截里面放的是热数据,一截里面放的是冷数据。

打住,不能再说了。

再说就是另一篇文章了,点到为止。

若是你不清楚,建议去学习一下哦。

LRU 在 Redis 中的应用

既然是内存淘汰算法,那么咱们经常使用的 Redis 里面必然也有对应的实现。

Redis 的内存淘汰策略有以下几种:

  • noenviction:默认策略。不继续执行写请求(DEL 请求能够处理),读请求能够继续进行。这样能够保证不会丢失数据,可是会让线上的业务不能持续进行。
  • volatile-lru:从已设置过时时间的数据集中挑选最近最少使用的数据淘汰。没有设置过时时间的 key 不会被淘汰。
  • volatile-random:从已设置过时时间的数据集中随机选择数据淘汰。
  • volatile-ttl:从已设置过时时间的数据集中挑选将要过时的数据淘汰。
  • allkeys-lru:和 volatile-lru 不一样的是,这个策略要淘汰的 key 对象是全体的 key 集合。
  • allkeys-random:从全部数据集中随机选择数据淘汰。

Redis 4.0 以后,还增长了两个淘汰策略。

  • volatile-lfu:对有过时时间的 key 采用 LFU 淘汰算法
  • allkeys-lfu:对所有 key 采用 LFU 淘汰算法

关于 Redis 中的 LRU 算法,官网上是这样说的:

https://github.com/redis/redis-doc/blob/master/topics/lru-cache.md

在 Redis 中的 LRU 算法不是严格的 LRU 算法。

Redis 会尝试执行一个近似的LRU算法,经过采样一小部分键,而后在采样键中回收最适合的那个,也就是最久没有被访问的那个(with the oldest access time)。

然而,从 Redis3.0 开始,改善了算法的性能,使得更接近于真实的 LRU 算法。作法就是维护了一个回收候选键池。

Redis 的 LRU 算法有一个很是重要的点就是你能够经过修改下面这个参数的配置,本身调整算法的精度。

maxmemory-samples 5

最重要的一句话我也已经标志出来了:

The reason why Redis does not use a true LRU implementation is because it costs more memory.

Redis 没有使用真实的 LRU 算法的缘由是由于这会消耗更多的内存。

而后官网上给了一个随机 LRU 算法和严格 LRU 算法的对比图:

对于这个图官网是这样说的:

你能够从图中看到三种不一样的小圆点造成的三个不一样的带:

  • 浅灰色带是被回收(被 LRU 算法淘汰)的对象
  • 灰色带是没有被回收的对象
  • 绿色带是新添加的对象

因为 Redis 3.0 对 LRU 算法进行了改进,增长了淘汰池。

因此你能够看到,一样使用 5 个采样点,Redis 3.0 表现得比 Redis 2.8 要好。

同时能够看出,在 Redis 3.0 中使用 10 为采样大小,近似值已经很是接近理论性能。

写到这里我忽然想起了另一个面试题。

数据库中有 3000w 的数据,而 Redis 中只有 100w 数据,如何保证 Redis 中存放的都是热点数据?

这个题你说它的考点是什么?

考的就是淘汰策略呀,同志们,只是方式比较隐晦而已。

咱们先指定淘汰策略为 allkeys-lru 或者 volatile-lru,而后再计算一下 100w 数据大概占用多少内存,根据算出来的内存,限定 Redis 占用的内存。

搞定。

才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在后台提出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。

还有,欢迎关注我呀。

相关文章
相关标签/搜索