MySQL架构(三)- 磁盘存储数据页

1  数据页

数据页是InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。InnoDB为了不同的目的设计了不同类型的页,比如存放表空间头部信息的页,存放Insert Buffer信息的页,存放INODE信息的页,存放undo日志信息的页等等。下面这些类型的页,我们都不会说,只讲用于存放数据的页,也就是数据页(也叫索引页)。

1.1  数据页结构

从图中可以看出,一个InnoDB数据页的存储空间大致被划分7个部分,有的部分占用字节数是确定的,有的部分占用的字节数是不确定的

1.2  数据在页中 的存储

在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:

2.3  Page  Directory (页目录)

记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:

最笨的办法:从Infimum记录(最小记录)开始,沿着链表一直往后找,总有一天会找到(或者找不到[摊手]),在找的时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了,因为该节点后边的节点的主键值依次递增。
大家发现当数据量很大的时候这样就行不通了,InnoDB的设计者受书目录的启发,为一个数据页中的数据也设计了一个目录。制作过程如下:

  • 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。

最终就是下面这样的:

注意图中行格式中省略了一些现在不看的数据。

而且每个分组(也就是槽)中的记录数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。

最后在一个数据页查找指定主键值的记录的过程分为两步:

1、通过二分法确定该记录所在的槽

2、通过该记录的next_record属性遍历该槽所在的组中的各个记录

2.4  Page Header (页头)

设计InnoDB的大叔们为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储页的各种状态信息,具体各个字节都是干嘛的看下表:

  • PAGE_DIRECTION

      假如新插入的一条记录的主键值比上一条记录的主键值比上一条记录大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION。

  • PAGE_N_DIRECTION

    假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。

2.5  File  Header(文件头)

上边唠叨的Page Header是专门针对数据页记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽了呀。我们现在描述的File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦吧啦吧啦~ 这个部分占用固定的38个字节,是由下边这些内容组成的:

对照这个表格,比较重要地方列举

  • FIL_PAGE_SPACE_OR_CHKSUM

这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。

  • FIL_PAGE_OFFSET

每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号来可以唯一定位一个页。

  • FIL_PAGE_TYPE

这个代表当前页的类型,我们前边说过,InnoDB为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的数据页,其实还有很多别的类型的页,具体如下表:

我们存放记录的数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页

  • FIL_PAGE_PREV和FIL_PAGE_NEXT

我们前边强调过,InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大(比方说一张表中可以有成千上万条记录),InnoDB可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,不过我们本集中唠叨的数据页(也就是类型为FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页其实是一个双链表,就像这样:

 

2.6  File Trailer

我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),设计InnoDB的大叔们在每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:

  • 前4个字节代表页的校验和

这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。

  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)

这个部分也是为了校验页的完整性的,只不过我们目前还没说LSN是个什么意思,所以大家可以先不用管这个属性。

这个File Trailer与FILE Header类似,都是所有类型的页通用的。

 

插入记录过程

如果Free Space的空间足够的话,直接分配空间来添加纪录,并将插入前最后一条纪录的next_record指向当前插入的纪录,将当前插入纪录的next_record指向supremum纪录。
如果Free Space的空间不够的话,则首先将之前删除造成的碎片重新整理之后,按照上述步骤插入纪录。

如果当前页空间整理碎片之后仍然不足的话,则重新申请一个页,将页初始化之后,按照上述步骤插入纪录

 

举例分析索引是怎么来解决问题

下图为记录对应的格式

record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录、1表示索引纪录(下文中会提到)、2表示最小记录、3表示最大记录

放入页中的示意图如下:

然后我们向表中插入数据

按照主键大小排列成单链表存储在页中(物理位置不一定是挨个排列的),效果图如下

此时我们再插入一条纪录

我们假设每个页中最多只能存在3条记录,所以需要申请一个新的页来存储,示意图如下:

 

为了显示效果,我们多插入几条纪录

这是我们可以每隔若干个页来抽取一个页来建立上层目录,类似于跳表的数据结构

目录项中只包含被抽中页中的最小的键值和其对应的页号。

那么目录项怎么存储呢?有没有发现跟数据页中的纪录很像?
所以,引入了索引页,其中的纪录的record_type为1.

然后我们迭代抽取页来建立上层索引,直至到一层只有一个页为止

上图的结构就是B+树。

当有了索引之后,我们如何查找数据呢?

跟二叉搜索树的查找过程极为相似,但是B+树有多个子节点,我们根据PageDictionary来快速查找目的的子节点,然后不断细化到叶子结点即可。
这种查找方式的效率比单链表的从头到尾遍历要高效很多倍

B+树的叶子节点存放的是我们的具体数据,非叶子结点是索引页。
所以B+树将数据分为了两部分,叶子节点部分和非叶子节点部分,也就我们要介绍的段Segment,创建两个Segment来存放对应的两部分数据。

Segment是一种逻辑上的组织,其层次结构从上到下一次为Segment、Extent、Page。
 

总结:

InnoDB为了更好的在磁盘中存储,设计页和行格式,并且规定一个页中最少有两行数据,页除了存储数据行,还包括一些数据页的描述部分,行格式中真是存储数据,并且也存储了这行数据相关的描述信息。

InnoDB数据页分为7个组成部分,各个数据页可以组成一个双向链表,而每个数据页中的记录会按主键值从大到小的顺序组成一个单项链表,每个数据页都会为存储在它里面儿的记录生成一个页目录、最大记录、最小记录,通过主键查找某条记录的时候可以在页目录中通过二分法快速定位到对应的槽,后再遍历该槽对应分组中的记录即可快速知找到指定的记录。页和记录的关系是:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值