我们先来了解什么是页?
(图1)
File Header和File Trailer都是记录通用信息的,Page Header是存储数据页的专有信息,这两部分我们不需要特别的去关注.我们需要关注的是其余四个Infimum+Supremum,User Records,Free Space ,Page Directory,
Infimum+Supremum:页面中最小和最大的记录,我们都知道一个页的大小一般是16KB,这里的最小和最大并不是指最小的id或最大的id,最小和最大是一个概念,只是代表最大值或最小值,比如插入表中3条数据,主键分别为1,2,3;那么infimum代表的含义就是,比1小的数,但没有实际具体数值。supremum就代表比3大的数,没有具体数值。他们并不表示真实的记录.
User Records:用户存储的记录内容,就是我们一页中的记录都存储在这里。
Free Space:页面中还没有使用的空间,Free Space和User Records两者的关系是数据相互的,例如User Records是空的,那么Free Space就是满的,
Page Direcetory:存储页中记录的相对位置,相当于是页的目录,用于快速定位到记录的
(图2)
我们可以看到"新申请的页"里由于没有记录,所以Free Space是满的,User Records这一部分是没有的,只有在"往页里面插入数据"时,两者才会同时存在,到了"插满数据"时,就只剩下User Records这一部分了,在"往页里面查数据"的时候,我们是从Free Space里面去申请空间,Free Space里面的这一部分空间就是属于User Records的,当"插满数据时",Free Space的全部空间就都属于User Records的,"插满数据"是一种很理想的状态,一般情况下是插不满的,剩下用不到的Free Space就会成为内存碎片.
(图3)
row_id:当我们的表里没有设置主键,没有设置union的列的时候,这一列就是记录的唯一标识,保证记录的唯一性
trx_id:事务id
roll_pointer:
变长字段长度列表:存储的是name字段,因为varchar是可变长类型,而int是定长类型,所以int类型的字段不存储在这里。
NULL值列表:存储的是可为空的字段。
记录头信息(很关键):
预留位1,预留位2:暂时不需要关注,可能到了高版本会使用到
deleted_flag:
物理删除(delete语句删除)&逻辑删除(is_deleted字段)
0:未删除1:已删除
deleted_flag属于逻辑删除,为什么不直接把它删除掉呢?
如果我们delete一次就删除一次,这会有一个性能消耗,删除一次就会把我们的记录重新排列一次.
删除操作:
1.把deleted_flag=1
2.把删除掉的记录,要组成一个垃圾链表.(目的:空间的可重用),我们新插入的数据也可以使用这个删除数据了的内存空间
min_rec_flag:B+树每层非叶子节点中标识最小的目录项记录.
n_owned:
把一个页划分成若干个组,一个组里的"大哥"(组里最大的主键)会保存该值,该值表示一个组里有多少条记录.例如:
一页有16KB,会存很多的记录.我们想象成这样
页=第15集团军的军队 记录=15万兵.而军队会分军师旅团营...,我们要把这15万兵细分下去
我们能看一下图4,因为Supremum是最大的,所以它就是组里的"大哥",它的n_owned是5代表这个组里有5条记录
heap_no:堆号,记录每条插入进来的记录都会分配堆号.从heap_no=2.因为InnoDB默认的Infimum的heap_no=0,默认的Supermum的head_no=1
record_type:
记录类型.
0:普通记录.(我们插入的记录)
1:B+树非叶子节点目录项记录.
2:表示Infimum的记录
3:表示Supermum的记录
next_record:
下一条记录.指向的是图3中"记录头信息"与"主键列的值"之间的"缝隙",这样的好处是,我往前查,可以得到"记录的额外信息",往后查,可以得到"记录的真实数据"
注意点:从当前记录的真实数据到下一条的真实数据的距离
(图4)
Page Directory(页目录)
- 最底层的记录特点
1.按照主键从小到大依次排序.
2.根据next_record的指向,我们能知道他是单向的链表,我们查询只能通过遍历单链表来查询,如果数据量过大,查询会非常耗时,所以我们衍生出了Page Directory.
- 图书的目录
- 分组的规则
1.对于Infimum记录,分组只能有1条记录,就是它自己.
2.对于Supermum记录,组里只能有1-8条记录.
3.对于其它的记录,组里只能有4-8条记录.
- 分组的步骤
首先:最初的时候,没有 记录,一个新的数据页有两个组,Infimum组1条记录,另一个是Supremum组1条记录.
之后:插入数据的时候,往Supremum组里插入,n_owned要加1.
最后:Supremum组满了,进行拆分,一个组拆分成2个组.(4+5),申请新的Slot(槽位),指向我们最大的记录.
- Slot(槽位)
指向的是组里最大的值,一个slot只会有一个组,InnoDB内部通过二分查找来定位所要查询的记录所属的槽位,然后再遍历所属的分组拿到指定的记录.(只在一页里面的查询过程)
(图5)
我们来看看删除记录操作,假如我们要删除id=3的记录,Mysql会将deleted_flag置为1,逻辑删除,还有id=2的next_record会指向id=4的记录,这样的删除对于整条链表来说代价是很小的,我们移动了指针,mysql就不会遍历到我们逻辑删除的记录了,还有要修改的是组里的"大哥"的n_owned要减1.我们再想象一下,如果这条记录还没有物理删除掉,内存空间也没有被别的记录替换掉,我们重新插入一条一模一样的记录,这时候要做的操作就是将delelte_flag置为0,再移动一下指针,最后将组里的"大哥"的n_owned加1.
(图6)
当我们乱序插入的时候,InnoDB为了保证主键有序,在底层会帮我们排好序,当User Records满了之后再执行插入操作,会开辟一个新页,当我们插入一个id=7的记录时,InnoDB同样为了主键有序,会做一次记录迁移,将id=7的记录保存在Page1,将id=10的记录迁移到Page2.这也是为什么InnoDB建议主键是自增的,这样就可以减少数据迁移的操作.
当我们进行大数据量(多页)中查询一条记录的时候,查询过程是怎么样的?
我们之前的单页查询,先二分查找Slot,在遍历组,在这种情况下失效了,因为页跟页之间没有任何联系,针对这种情况,引出了B+树(这里不开展介绍)
Buffer Pool
缓存池, 我们查询的速度会被磁盘限制,我们为了加快查询速度,就会存到比较快的内存里面
索引,数据------------>页-------->磁盘里的
例如:select * from tb_student where id =1;我们查询的是一条记录,但是我们要把该记录所在的页里的所有记录都加载到内存中,如果不在一个页里的话,那就会有大量的io操作,针对这种情况,buffer pool就产生了
某一刻,会将脏页刷新到磁盘?什么情况下刷新?
1.从flush链表中刷新一部分页到磁盘上.
后台有线程会根据系统的繁忙程度来确定刷新的速率.这个频率是动态的变化.BUF_FLUSH_LIST
系统很繁忙的时候,会造成我们刷新脏页到磁盘的速度很慢.当用户从磁盘去加载页到buffer_pool的时候,我们buffer_pool就没有可用的 缓冲页,这时候就会去查看LRU链表尾部.是不是存在可以直接释放掉的未修改的缓冲页.如果没有,不得不将LRU链表尾部的一个脏页同步刷新到磁盘中,等于是强制刷新脏页到磁盘上. BUF_FLUSH_SINGLE_PAGE
2.从LRU链表的冷数据汇总刷新一部分页到磁盘 BUF_FLUSH_LRU ;LRU(Least Recently Used)最近最少使用.
(图7)
我们的操作系统申请了一片连续的内存空间,而这片空间是用来存储页的,我们看看Innode_buffer_pool_size的设置是不包括控制块的,Innode_buffer_pool_size的默认大小是128MB.
如何判断一个页是否放到buffer_pool里面?InnoDB底层是有一个哈希表,key:表空间+页号 value:对应缓冲页的控制块;因为我们是通过控制块来获得页的状态,是否被修改,同步等,控制块非常重要
(图8)
我们来介绍free链表,空闲的链表,我们控制块是如何维护的,它其实是一个双向链表,还有一个"基节点",它能快速定位到链表的"头节点"和"尾节点",count就是控制块的数据量;控制块里面有记录着"前节点"和"后节点",当Mysql初始化buffer_pool的时候,就如同图8这个样子的,只有free链表,我们free链表存的是控制块,并不是缓冲页,控制块和缓冲页是一对一对的存在的,当插入数据的时候,我们会先去判断我们这个数据有没有空间插入到buffer_pool里面,那么怎么判断呢?就是看我们的free链表,当我们发现这条链表里面有节点的时候,就代表有空闲的,这时候会把空闲的缓冲页对应的链表中的控制块拿出来,将要插入的数据的页放进缓冲页里面,由于这个控制块已经不是空闲的了,所以会从链表中去掉,最后剩余的控制块,就是我们可以使用的控制块.
(图9)
我们再来看看flush链表,我们buffer_pool里面有数据了,我们的数据时可以修改的,当我们修改一条数据的时候,并不是直接修改磁盘上的数据,而是修改buffer_pool里面的数据,磁盘的数据还没有动,然后就会有一个问题,我们缓冲的数据与磁盘上的数据不一致,可以称之为"脏页",
当出现"脏页"之后,不能马上的刷到磁盘里面去,这个"脏页"会在未来的某一个时刻把它刷进去,我们修改的数据不可能都在一个页里面的,如果有被修改的页,Mysql就会把对应的控制块存放到"flush链表"里面.我们就能知道flush链表的作用了,如果我们要将"脏页"刷到磁盘上面的话,我们只需针对这个链表做操作就好了.
一个缓冲页的控制块是不可能同时存在free链表和flush链表里面的
(图10)
假设buffer_pool满了,我们肯定要把有用的数据缓冲下来,没用的数据剔除掉,所以就有了LRU链表,假设我们访问了n个页,我们在buffer_pool里面已经缓存了m个页了,而我们的命中页的几率就是m/n,这个几率越大,访问我们的buffer_pool就越多,减少从磁盘中加载数据到buffer_pool的次数,效率也越大,但是我们要想办法提高这个命中率,buffer_pool会有一个预读的概念,
预读分两种,1.线性预读. 2.随机预读(默认不开启),预读是加载进来不读,预读会刷走buffer_pool的页.
全表扫描:一个页一个页的去加进buffer pool里面去扫,最后因为这次全表扫描,导致之前缓冲的页全部清没了.加载进来就读.
针对这两个问题,Innodb会有这样一个机制,页初次加载还没有被访问的时候,都只会往old区域里扔,所以只会刷到LRU链表的old区域,InnoDB会有个参数设置,某时刻的间隔内例如1000ms,不会让old区域的页加到young区域里面.
(图11)
默认buffer_pool是一个,但是可以是多个的,我们内存超级大的时候,就可以设置多个buffer_pool,一个chunk代表一片连续的内存空间.