什么是LRU算法?LRU算法在Redis和MySQL是如何应用的?

283 阅读6分钟
前言

LRU 算法的实现是一道互联网面试中常考的题目,作为一种数据访问策略,LRU算法在操作系统、各种中间件也很常使用,并且根据实际情况也有一些优化。今天主要讲讲什么是LRU算法和实现,以及在Redis和MySQL中的实际应用和改进。

LRU算法

什么是LRU算法

LRU全称是Least Recently Used, 也就是最近最少使用,是一种常用的数据访问和淘汰策略。

为了避免每次访问数据都经历从磁盘中加载的耗时过程,我们选择用有限空间的内存来缓存一部分经常使用的数据,由于无法缓存全部的数据,所以需要有数据淘汰策略。LRU算法的策略如下:

  • 数据被访问时更新数据的访问频率,如果加载数据到缓存时缓存已满,则淘汰最近最少被使用的数据
举个栗子

比如现在的缓存空间是3,访问数据的顺序如下:

访问1, 1未在缓存中,所以从数据库加载到缓存中,此时缓存中的数据为[1];

访问2, 2未在缓存中,所以从数据库加载到缓存中,此时缓存中的数据为[2,1],由于2是最新被访问的数据,所以2在1前面;

访问3, 3未在缓存中,所以从数据库加载到缓存中,此时缓存中的数据为[3,2,1];

访问4, 4未在缓存中,所以从数据库加载到缓存中,此时缓存已满,需要淘汰最近最少使用的数据,也就是1,此时缓存中的数据变为[4,3,2];

访问3,此时缓存中的数据变为[3,4,2];

具体实现

LRU缓存可以用双向链表和哈希表实现,其中哈希表记录对应数据在链表中的节点位置。每当数据被访问时,对应数据节点插到链表头部,当列表长度大于容量时,淘汰尾部节点。用双向链表是为了当访问的某个节点在缓存中时,可以获取到节点对应的前驱节点,方便将该节点从链表中删除并移到头结点。

例如上述例子在双向链表的表示如下,其中缓存空间大小容量为3:

访问1;

访问2;

访问3;

访问4;

访问3;

Redis 内存淘汰的LRU过期策略改进

Redis 是一种内存型数据库,当缓存中的数据量大于Redis的最大内存时,Redis采用配置的内存淘汰策略淘汰数据,其中LRU内存淘汰策略是redis的淘汰策略之一(volatile-lru和allkeys-lru)。

但Redis并没有采用上述描述的链表加哈希表的实现方式来实现LRU淘汰策略,这是因为Redis本就是基于内存的数据库,维护一个拥有所有键值对的链表会占用大量的空间,另外每次访问数据时频繁的移动数据到链表头部也需要开销。因此Redis采用的改进方案是在Redis对象结构体中新增一个访问时间字段,采用随机采样的方式淘汰数据,即每次取N个值,淘汰最近最久未使用的键值对。

MySQL Buffer Pool的LRU策略应用

Buffer Pool 是Mysql InnoDB 引擎的缓存池,数据从磁盘中读取出来之后会缓存在Buffer Pool, Buffer Pool 的大小也是有限制的,因此需要设计内存淘汰策略,Buffer Pool的内存淘汰策略是在LRU算法上的改进。

Mysql LRU算法的列表分为两个子列表,如下图所示:

其中头部young区域是最近访问过的新页面的子列表,尾部old区域是最近访问次数较少的旧页面的子列表。

为什么要分成两个子链表?

这是因为Innodb存储引擎在读取某条记录对应的页数据时,还会将相邻的其他页的数据一并读取出来,这是遵从计算机体系结构和操作系统设计的空间局部性原理,即如果一个数据项被访问,那么与它相邻的数据项很可能也会被访问。

但相邻页也可能并不会被真正访问到,假设被读取后一并插入到链表的头部,就有可能占用真正被实际访问的数据页在链表中的空间,为了应对这种预读失效的情况,Mysql进行了以下改进:

1.当 Innodb 将一个页面读入缓冲池时,它最初会将其插入old区域的子列表的头部;

2.如果old区域的子列表的页面被访问,那么该页面会被移动到young区域的子列表的头部。如果该页是因为用户访问被读取的,那么第一次访问将立即发生且该页将被移动到young区域的子列表的头部;

假设该页是因为预读操作被读取的,那么第一次访问不会立即发生,也就是预读页将会停留在old区域的子列表。如果该数据页一直不被访问,那么该数据页也不会占用young区域的子列表的空间,随着数据库的运行,一直不被访问的页面也会到达old区域的子链表的末尾,最终被淘汰出链表。

3.这种机制下的LRU算法会有一个问题:扫描大量数据时比如全表扫描会导致大量只在本次查询被访问一次的页面被加载进young区域的子链表中,会引起大量原本在链表中的热点数据页被淘汰,比如select * 语句, 即Buffer Pool被污染,需要针对这种情况做处理。

innoDB 存储引擎的处理方式是配置参数 innodb_old_blocks_time,指定处在 old 区域的子链表在第一次访问页面之后的时间窗口(以毫秒为单位),在该时间窗口期间,访问页面将不移动到LRU链表的young区域的子链表。这样就可以防止因为大量扫描导致只访问一次的数据页被加载进young区域的子链表导致热点数据页被淘汰的问题。

innodb_old_blocks_time 的默认值为 1000 。

参考链接

力扣LRU缓存题目:leetcode.cn/problems/lr…

MySQL文档: dev.mysql.com/doc/refman/…