前面有一篇文章里面我写了LRU算法,其中还将一些算法更新了一下,现在我就做个笔记,写一下LRU算法的一些用途,另外说明一下,这里我们LRU算法都不会考虑空间不足的问题(因为从软件模拟来说基本不会存在这个问题),如果在CPU硬件设计上出现空间不足的话,我们直接将链表的第一个元素剔除即可,当然,也有相应硬件能够直接做这种事情。
当然,LRU不仅仅在硬件设计上有很好的作用,在软件设计上也有很好的作用,例如Orical的数据库就是用了LRU算法。我们可以先看看下面这个图。

Oracle的设计可以从上图看到,这里buffer(缓冲区)就像一个水池,水池的最小单位就是数据块。当每个数据块被读入buffer时,Oracle或相应的软件都会抽取数据块的头部,在内存中构建buffer header,并将这些buffer header串成链表的形式。而buffer header里面记录的指针就指向buffer中的该数据块本身。于是,当我们在搜索某个数据块时,就不用去缓冲区中找,而是直接扫描链表上该数据块所对应的buffer header,然后根据找到的buffer header所记录的指针就能到buffer中直接定位该数据块了。而维护buffer header就是hash表,而buffer header就是一个LRU维护的链表。
这里的buffer head就是数据的一些信息,例如如果我将数字存储到数据中,我可以通过一种方式获取其head的信息,存放到相应的hash组里面,而hash的key也是可以通过数据本身得到。例如我们可以将数字求100的余数得到key,然后余10得到页面id。例如25这个数字,我们可以余100为25,余10为5,那么我们就在缓冲区buffer[25,5]的地方将这个数据保存起来,下次我们取的时候可以直接从25获得hash的组id和存放的页面id,即25和5,则我们直接读取buffer[25,5]即可。
说了这么多,可以稍微总结一下,这个数据结构就是。
- hash表:存放数据的组,用于查找和维护。
- buffer head:LRU链表,被hash组维护,其内容为在内存(缓冲区)的页面id。
- buffer:缓冲区,用于存放数据。
简单的说,也就是当数据读入,我们通过一种方法获取组的id,然后在相应的组中维护一个页面id(链表),然后查找的时候直接通过组id和页面id得到数据,从而提高速度。
不过这里就会有一个问题,hash表这里就不存在冲突的问题了,因为hash表的value是一个链表结构,所以不存在冲突,如果有相同的key的话,我们直接在后面添加节点即可,但是在缓冲区的话就有可能有问题,例如25和125这两个值最后在缓冲区的位置应该是一样的,那么我们肯定不能覆盖掉,所以我们就查找25这个位置(25,5)后面的这个空间是否被使用(25,6),如果没被使用,则我们写到这个空间中,并更改LRU链表指针里面的值。
例如25会是第25组,其页id为5,当读到125的时候,发现也是25组和id为5,那么我们就查找6的位置,如果6位置没数据写入,那么OK,写到6的位置,并更新链表为第25组,页id为6,直到内存缓冲区被写满为止,我们都不覆盖。
这样我们解决了冲突问题,但是这样的话,查找也就出了问题,即不能够直接查找,同样是25和125这两个数,两个坐标最后会成为(25,5)和(25,6),但是我们两个数通过规则获得到的key和页面id是25和5。所以当我们查找的时候就不需要使用页面id,而是采用扫描的方式。即当我们搜索125这个数字的时候,可以获得组id,即25,然后扫描这个组维护的LRU链表,然后扫描内存块,查找是否是我们需要的数据,如果是,则返回。就是我们搜索125,发现组id为25,那么我们就找(25,5),发现数据为25,不是我们想要的,继续下一个链,(25,6)直到找到需要的数据。
不过上面我们也可以优化一下,即优化成结构体,每个数据都有自己的独立的页面id,而不是通过一种规则去找页面id,尽量避免冲突,提高效率。我们可以看一下Orical的做法。
“当前台进程发出SELECT或者其他DML语句时,Oracle根据SQL语句的执行计划找到符合SQL条件的数据块,然后Oracle会根据对请求的数据块的地址以及数据块的类型作为参数,应用hash函数以后,得到要找的数据块所处的hash bucket,也就是确定该数据块在哪条hash chain上。然后,Oracle进入该hash chain,从上面所挂的第一个buffer header开始,根据buffer header所含有的指针找到对应的块体,然后扫描其中的数据,确认其是否是SQL语句所需要的块,如果是,则返回该块里所需要的数据;否则,如果不是,则继续往下搜索,一直搜索到最后一个buffer header为止。如果一直都没有找到,则调用物理I/O,到数据文件里把该块所含有的内容复制一份到一个可用的buffer里,并构建该块的 buffer header,然后将该buffer header挂到hash chain上去。”
所以基本上我们可以明确算法了,下面我给一下C#模拟的代码。
namespace LRUandData
{
定义LRU链表
class Program
{
定义数据块,缓冲区和hash表
static void Main(string[] args)
{
hash = new Dictionary<int, LRUList>();
//初始化数据
InsertData();
//查询数据
int value = SearchData(33);
Console.WriteLine(value);
Console.ReadKey();
}
初始化数据
查找数据
}
}
上面的代码模拟了基本的操作,如果有更多功能只能大家自己改了。:)
从这里我们也可以看到,设计一个好的软件,数学,数据结构,计算机组成原理等等基础的东西是非常重要的,决定技术高度,基础的牢固程度最重要,举个老师给我们举了一百遍的例子,房子的高度由地基牢固程度决定,真的一点不假。

[...] LRU算法的一些用途 [...]
看了你的文章,收益良多……
谢谢了!
@游客
不客气,能帮助到你一点点我也觉得很高兴。