缓冲池是数据库最终的概念,数据库能够将一部分数据页放在内存中造成缓冲池,当须要一个数据页时,首先检查内存中的缓冲池是否有这个页面,若是有则直接命中返回,没有则从磁盘中读取这一页,而后缓存到内存并返回。算法
可是内存的价值较高,通常来讲服务器的内存老是小于磁盘大小的,并且内存不能彻底分配给数据库做为缓冲池。这就意味着数据库基本上没法将全部的数据都缓冲到内存中。数据库
当缓冲池满后,若是还有新的页面要被缓冲到池中,就要设计一种页面置换的算法,将一个旧的页面替换成新的页面。缓存
通常来讲咱们熟悉的算法有下面几种:服务器
下面逐一介绍各类算法。性能
若是被替换掉的页是之后不再会使用的,那么这种算法无疑是最优秀的。由于无论什么算法,替换掉的页也有可能再次被缓存,替换掉其它的页。测试
可是这种算法是没法实现的,咱们不可能知道哪一个页面之后也在不会被使用。设计
或者咱们退一步,将这个算法改为被替换掉的页是之后很长一段时间都不会再次被使用的,那么这种算法无疑也是最优秀的。指针
可是仍是会面对一个没法实现的问题,咱们仍是不知道哪些页面会在将来多长一段时间内不会被再次访问。页面没法确认,时间也没法肯定。blog
虽然这种算法没法被实现,可是能够做为一种度量,若是有一种算法其效率最接近OPT,那么这种算法无疑是优秀的算法。队列
先进先出算法是一种很简单的算法,其基本思想是造成一个队列,最早入队的页面最早被逐出。咱们用示意图来模拟一下FIFO算法:
咱们的内存假设只能保存4个页面,此时的访问请求按照时间顺序是1->2->3->4->5,那么按照时间顺序,当访问到4号页面时队列正好填满,当要访问5号页面时,会将最早入队的1号页面逐出。
这种算法实现起来很简单,可是从实现上来看,性能和OPT算法差距最大。由于被替换出去的页面颇有多是最常使用的页面,所以这个算法不多见出如今数据库缓冲池管理中的。
FIFO算法会出现一个叫作Belay异常的现象,就这个现象咱们解释以下。
咱们首先定义一个4个页面长度的队列做为缓冲池,而后按照下面的顺序访问:1->2->3->4->5->3->9->1->4->2->7->4->7。那么咱们按照刚才描述的FIFO来看看访问的过程:
访问顺序 | 访问页 | 内存队列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 2,3,4,5 | 否 |
6 | 3 | 2,3,4,5 | 是 |
7 | 9 | 3,4,5,9 | 否 |
8 | 1 | 4,5,9,1 | 否 |
9 | 4 | 4,5,9,1 | 是 |
10 | 2 | 5,9,1,2 | 否 |
11 | 7 | 9,1,2,7 | 是 |
12 | 4 | 1,2,7,4 | 否 |
13 | 7 | 1,2,7,4 | 是 |
从这个表格上看到,非命中次数有9次,那么咱们将这个队列的容量增长到5,而后再次重复这个访问序列,看看效果:
访问顺序 | 访问页 | 内存队列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 1,2,3,4,5 | 否 |
6 | 3 | 1,2,3,4,5 | 是 |
7 | 9 | 2,3,4,5,9 | 否 |
8 | 1 | 3,4,5,9,1 | 是 |
9 | 4 | 3,4,5,9,1 | 是 |
10 | 2 | 4,5,9,1,2 | 否 |
11 | 7 | 5,9,1,2,7 | 否 |
12 | 4 | 9,1,2,7,4 | 否 |
13 | 7 | 9,1,2,7,4 | 否 |
这样的话,非命中的次数是10次,奇怪的是增长了缓冲池的容量,非命中缓冲的数量还增长了,这种现象就叫作Belay异常。
这种算法不该该被考虑。
LRU算法的思想也很简单,实现一个链表(双向链表),每次要缓冲新的页面时,遍历链表,选择最近最少使用的页面进行逐出操做。
这种算法要求每一个页面上记录一个上次使用时间t,程序决定逐出时,以这个时间t为准,t距离当前时间最大的,就是要被逐出的页面。
下图中按照1->5->2->2->6->5->4的顺序访问,内存和访问示意图以下:
其中最接近顶端的页面咱们认为其t最小,最接近底部,咱们认为其t最大。
访问6号页面的时候,内存被填满,下一次访问5号页面的时候,会将5号页面提高到顶部,也就是t最小,以后访问4号页面,由于原先内存中没有4号页面,所以会选择逐出一个页面。此时1号页面在底部,其t最大,所以被逐出。
那么LRU算法是否解决了Belay异常呢?
仍是按照上一节的实验顺序,测试容量为4和5的内存,左侧到右侧,t逐渐增大:
访问顺序 | 访问页 | 内存队列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 2,3,4,5 | 否 |
6 | 3 | 2,4,5,3 | 是 |
7 | 9 | 4,5,3,9 | 否 |
8 | 1 | 5,3,9,1 | 否 |
9 | 4 | 3,9,1,4 | 否 |
10 | 2 | 9,1,4,2 | 否 |
11 | 7 | 1,4,2,7 | 否 |
12 | 4 | 1,2,7,4 | 是 |
13 | 7 | 1,2,4,7 | 是 |
一共有10次未命中。增长容量到5,看一下新的状况:
访问顺序 | 访问页 | 内存队列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 1,2,3,4,5 | 否 |
6 | 3 | 1,2,4,5,3 | 是 |
7 | 9 | 2,4,5,3,9 | 否 |
8 | 1 | 4,5,3,9,1 | 否 |
9 | 4 | 5,3,9,1,4 | 是 |
10 | 2 | 3,9,1,4,2 | 否 |
11 | 7 | 9,1,4,2,7 | 否 |
12 | 4 | 9,1,2,7,4 | 是 |
13 | 7 | 9,1,2,4,7 | 是 |
未命中的次数已经变成了9次,减小了一次,若是我设计的队列中有大量的重复,那么这个改进应该更加明显。
LRU算法在InnoDB的实现中是被改进的,每次新添加进去的页面会被放在队列的3/8处。
不管如何,LRU算法都被认为是最接近OPT的算法。
时钟置换算法能够认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。咱们给每个页面设置一个标记位u,u=1表示最近有使用u=0则表示该页面最近没有被使用,应该被逐出。
按照1-2-3-4的顺序访问页面,则缓冲池会以这样的一种顺序被填满:
注意中间的指针,就像是时钟的指针同样在移动,这样的访问结束后,缓冲池里如今已经被填满了,此时若是要按照1-5的顺序访问,那么在访问1的时候是能够直接命中缓存返回的,可是访问5的时候,由于缓冲池已经满了,因此要进行一次逐出操做,其操做示意图以下:
最初要通过一轮遍历,每次遍历到一个节点发现u=1的,将该标记位置为0,而后遍历下一个页面,一轮遍历完后,发现没有能够被逐出的页面,则进行下一轮遍历,此次遍历以后发现原先1号页面的标记位u=0,则将该页面逐出,置换为页面5,并将指针指向下一个页面。
假设咱们接下来会访问2号页面,那么能够直接命中指针指向的页面,并将这个页面的标记为u置为1。
可是考虑一个问题,数据库里逐出的页面是要写回磁盘的,这是一个很昂贵的操做,所以咱们应该优先考虑逐出那些没有被修改的页面,这样能够下降IO。
所以在时钟置换算法的基础上能够作一个改进,就是增长一个标记为m,修改过标记为1,没有修改过则标记为0。那么u和m组成了一个元组,有四种可能,其被逐出的优先顺序也不同: