缓冲池 buffer pool
innodb中的数据是以【页】的形式存储在磁盘上的表空间内,但是【磁盘的速度】和【内存】相比简直不值一提,而【内存的速度】和【cpu的速度】同样不可同日而语,对于数据库而言,I/O成本永远是不可忽略的一项成本,我们不妨思考下面的小问题:
小问题:一个全表扫描会产生有多少次磁盘I/O?
select * from user where id between 10 and 1000;
- 访问id为1的数据,需要访问当前表空间的第一行数据,一次I/O
- 访问id为2的数据,需要访问当前表空间的第二行数据,两次I/O
- 访问id为3的数据,需要访问当前表空间的第三行数据,三次I/O…
我们发现id为1,2,3…的数据都在同一个【数据页】,这会导致一个严重的问题,一次简单的查询,会访问【同一个页很多次】,可能产生很几百次I/O操作。所以为了解决快如闪电的【cpu】,和慢如蜗牛的【磁盘】之间的矛盾,innodb设计了buffer pool,我们直接读取数据所在页和相邻的页到缓存,有了缓存之后我们的执行过程如下:
- 访问id为1的数据,需要访问当前表空间的第一行数据,缓存当前页,一次I/O
- 访问id为2的数据,需要访问当前表空间的第二行数据,从缓存获取,无需I/O
- 访问id为3的数据,需要访问当前表空间的第三行数据,从缓存获取,无需I/O…
Innodb引擎会在mysql启动的时候,向操作系统申请一块连续的空间当做buffer pool,空间的大小由变量innodb_buffer_pool_size
确定,我这台电脑他使用了8G,你的可能是128M。
一、缓冲池的内部结构
整个buffer pool是由【缓冲页和控制块】组成的:
- 缓冲页:buffer pool中存放的【数据页】我们称之为【缓冲页】,和磁盘上的数据页是一一对应的,都是16KB,缓冲页的数据,是从磁盘上加载到buffer pool当中的一个完整页。
- 控制块:他是缓冲页【描述信息】,这一块区域保存的是数据页所属的表空间号,数据页编号,数据页地址,以及一些链表相关的节点信息等,每个控制块大小是缓存页的5%左右,大约是800个字节。
其内部结构如下,buffer pool的前一部分存储【控制块】,后一部分存储【缓冲页】,如果中间有未被利用的空间,就叫他【内存碎片】吧:
buffer pool的初始化:
数据库会在启动的时候,按照配置中的Buffer Pool大小,去向操作系统申请一块内存,作为Buffer Pool的内存区域,然后会按照默认的缓存页的的大小【16KB】以及对应的【800个字节左右】的【控制块】的大小,在Buffer Pool中划分出一个一个的缓存页和一个一个与其对应的描述数据(控制块)。此时的buffer pool像一个干净的本子,没有书写任何内容。
二、free链
刚初始化的buffer pool,内存中都是【空白的缓冲页】,但是随着时间的推移,程序在执行过程中会不断的有新的页被缓存起来,那怎么来判断哪些缓冲页是【闲置状态】,可以被使用呢,此时就需要【控制块来进行标记和管理】了。
innodb在设计之初,会将所有【空闲的缓冲页】所对应的【控制块】作为一个个的节点,形成一个链表,这个链表就是free链,这个链表就是一串未被使用的控制块,翻译过来就是空闲链表,如下图:
由上图可知,free链表是一个双向链表,链表上除了控制块以外,还有一个基础节点,存储了free链有多少个描述信息块,也就是有多少个空闲的缓存页,以及指向链表头尾的指针。
当我们加载数据的时候,会从free链中找到空闲的缓存页,把数据页的【表空间号和数据页】号写入【控制块】。
加载数据到缓存页后,会把缓存页对应的控制块从free链表中移除。
(1)、怎么知道数据页是否被缓存?
我们已经有了free链表用来【保存空闲的页】,但是,当下一次访问时,要如何知道当前要访问的页是不是已经被缓存了,最直观的思路就是将buffer poll里的缓存数据【全部遍历一遍】。
显然,这样做并不合理,本来设计buffer pool是为了提升效率,如果有人将buffer pool配置的很大,比如32个G,那扫描这一片区域的功夫都可以喝一杯茶了,反而成了累赘。
事实上,我们使用的hash算法,使用【表空间号+页号】进行hash就可以确定一个唯一的页。
那么我们能不能设计一个hash表,使用【表空间号+页】号当做key,使用【控制块地址】做value,每次查询的时候只需要通过key进行查找即可,大家都知道hash的时间复杂度是O(1),这样就能迅速定位缓存的页。(和hashmap很像)
结合我们的free链表,查询/缓存一个页的流程大致如下:
三、flush链表
(1)、脏页
在sql的执行过程中,无论是增删改查,都是优先在buffer pool中进行的,这样可以极大的保证执行效率。
但是同样会有一个问题,假如我们对缓存页的某些数据进行了修改(执行了一条update语句),就会导致buffer pool中的缓冲页和磁盘的数据页【数据不一致】,那么此时的缓冲页就称之为【脏页】。当然,这也就说明了,脏页的数据是要刷到磁盘上的。
(2)、链表结构
- flush链表同样是一个双向链表,链表结点是被【修改过的缓存页】的控制块。
- 和free链表一样,flush链表也有一个基础结点,链接首尾结点,并存储了有多少个控制块。
(3)、刷盘时机
后台会有专门的线程每隔一段时间就把flush链表中的脏页刷入磁盘中,刷新的速率取决与当前系统是否繁忙。
在这样的机制下,万一系统奔溃,是会产生数据不一致的问题的,没有刷入磁盘的数据就会丢失,而mysql通过日志系统解决了这个问题。
四、LRU链表
(1)、概述
内存是有限的,buffer pool更是有限的;缓存只是数据的中转站,当我们的数据量很大以后,buffer pool其实是仅仅能容纳很少一部分数据,所以buffer pool的容量很有可能被使用殆尽,如果此时我们还想继续缓存数据页那该怎么办?
合理的做法就是,当需要更多的空间缓存【新的数据页】的时候,我们将最近使用最少的【缓冲页淘汰掉】就可以了,这就是典型的LRU(最近最少使用)算法,对于innodb而言,是通过【LRU链表】来完成此功能的,他的结构和free链表、flush链表基本相同,只是负责的功能不同而已。
于是,一个简单的思路诞生了,当客户端访问一条数据时,会加载对应的数据页到buffer pool,并会将缓冲页对应的控制块放置到【LRU链表的首位】。一旦buffer pool被占满,则从链表的末端开始淘汰数据,这是最简单的实现。
(2)、优化
但是,实际的在使用场景中,我们需要对原有的LRU链表进行优化,因为他在一下场景可能会出现一些问题:
- 数据页预读:我们在讲多线程的时候是讲过【预读性原理】(当一个应用在访问一个数据时,很有可能会继续访问和他相邻的数据),cpu的高级缓冲区读取主存的数据也不是一个字节一个字节的读取,而是一下子会读取一个【缓存行】。同理,innodb从磁盘读取数据,也不一定是一页页读取,当mysql读取当前需要的页时,如果觉得后续操作会使用【附近的页】,就会将他们一起缓存到buffer pool,这样的作用是为了提升效率。但是,这也会导致大量的使用频率并不高的数据放置在LRU链表头部,反而将一些真正的【热点数据】淘汰。
- 全表扫描:一条【select * from user】 语句,会直接将一张表的全表数据缓存,并全部放在LRU链表头部,一样会淘汰很多热点数据。
所以,innodb对该链表进行了优化,将【LRU链表】分成了两个区域,分为【热数据区】和【冷数据区】,默认情况下冷数据区占了总链表的37%,机构如下:
一个select语句可能会多次访问一个页,因为你有【很多数据是保存在同一个页内】的。对于一个全表扫描的语句,每访问一条数据,就会访问一次相关的页,所以缓存确实能极大的提升效率:
-
对于预读的数据页,会在第一次访问时放入old区域,如果在sql执行的过程中访问相邻数据时,再次访问访问到该数据页,则把他加入如热数据区。
-
【大表的全表扫描】是个使用频率很低的操作(小表怎么操作都无所谓),但是如果按照上边的操作,首先全表数据会被放在【old区】,全表扫描必然会因为访问相邻数据而产生第二次、第三次、甚至数百次的访问,也就以为着这些页面会被全部放在young区。为了解决这个问题,INnodb提供了这样一个参数【innodb_old_blocks_time】,默认是1s,他的执行流程大致如下:
1、页被首次访问时会记录访问的时间戳。
2、以后访问都和首次访问的时间进行对比,如果时间大于1s,就讲当前页放入yong区。
3、一个sql的扫描一个页的时间,哪怕在慢也不会低于1s,这样就解决了一个全表扫秒而导致全表成为热点数据的问题。
tips:也就意味着,热点数据要求首次访问时间和最后一次访问时间的时间差不能低于1s。
tips:使用以下的语句,可以查看innodb当前的状态:
show engine innodb status
Total large memory allocated 8585216 # 为innodb 分配的总内存数(byte)
Dictionary memory allocated 446370 #为innodb数据字典分配的内存数(byte)
Buffer pool size 512 #innodb_buffer_pool的大小(page)
Free buffers 101 #innodb_buffer_pool lru列表中的空闲页面数量
Database pages 277 #innodb_buffer_pool lru列表中的非空闲页面数
Old database pages 0 #innodb_buffer_pool old子列表的页面数量
Modified db pages 273 #innodb_buffer_pool 中脏页的数量
Pending reads 1 #挂起读的数量
Pending writes: LRU 0, flush list 0, single page 0 #挂起写的数量
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 8002054, created 766955, written 4652116
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
Buffer pool hit rate 993 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 277, unzip_LRU len: 0
I/O sum[22009]:cur[1940], unzip sum[0]:cur[0]