MySQL是怎样运行的——第九章

这一章主要是关于表空间的内容。我们可以把表空间看作是页池

另外这一章的逻辑比较复杂,层级关系较多。在阅读书籍/我的博客时,一定要牢记这个顺序:表空间 > 组 > 段 > 区 > 页

9.1

这一节主要是复习前面章节的知识。
首先是常用的页面类型有哪些:
MySQL是怎样运行的——第九章
其次是页面的通用部分。基本是任何种类的页面都有File Header和File Trailer。其中后者主要是用于校验。前者的组成相对比较复杂:
MySQL是怎样运行的——第九章

9.2

这一节内容很多。都是关于独立表空间结构的。
首先要介绍的是“区”这个概念。

每个区默认1MB。对于16KB的页来说,就是64个页。

之所以引入区的概念,是为了降低随机磁盘io。
传统的数据库都是使用机械硬盘的,机械硬盘的主要io开销在磁臂旋转上,如果是随机磁盘io,则磁臂会不停的转来转去,效率低。
如果想避免这种情况,就需要让磁臂尽量少的旋转,想要实现这种场景就需要让数据页在磁盘中连续存储。这样磁臂就不会乱转,只会一次往下转一点点。
而想要让磁盘中的页连续,就需要在申请空间的时候申请一大块磁盘空间,而非一个单独页那么大的空间,那样很有可能不连续。
因此,我们有了区的概念。一个区由64个页组成,以区为单位进行申请,就会很大程度上避免页的存储不连续问题。

而256个区放在一起又会形成“组”。

根据层级关系可知,每个组对应着大量的页面。而其中必然有一些页面是用来描述组的元信息以及区的元信息的(其实还有段的一些元信息)。描述这些信息的页是第一个组的前三个页和其他所有组的前两个页。

MySQL是怎样运行的——第九章
下面介绍一下这些页面都是干什么用的。

FSP_HDR:这个页面记录了表空间的元信息以及本组所有的区的元信息
XDES:和FSP基本相同,也是记录本组所有区的元信息。区别在于它不会再记录表空间的元信息了。
IBUF_BITMAP:用来存储change buff的元信息。change buffer主要是用来缓存操作的。对磁盘上页面的修改操作会被缓存在change buffer中,等到页面因为某些事件实际加载到内存中时再进行执行。原来change buffer只缓存插入操作,因此原来叫insert buffer。
INODE:记录了段的相关信息。段会在后面进行介绍。

之前提到区是一组页面的集合。对于B+树来说,一组页面里既有叶子节点,又有内部叶节点。如果区中什么都存储,则会让我们在遍历叶子结点的链表时效率很低。
因此,我们会把一类页面放到一些区中,另一类放到不同的区中。这种只存储一类页面的区的集合就叫段。从这里也不难看出,段是个逻辑概念

另外,段还不只是区,段是区和一些零散页面的组合。之所以这么说,是因为有些页面既不属于叶子结点那个区,又不属于内部叶节点那个区,可它们也应该被划分到段中。所以严格来说,段是区和零散页面的组合

那么什么样的页面不是内部叶子结点那个区,也不是普通叶子结点那个区呢?这里要引出一个概念,就是碎片区。

碎片区

由上文可知,内部叶节点区的集合 和 叶子结点区的集合分别对应一个段。可是即使每种页面只有一个区,那也需要2MB的空间。如果一个表里还没什么记录就要2MB的空间,那也太多了吧??

因此,当每个表刚开始有数据的时候,我们会把这些数据存放到碎片区的页面里。碎片区里什么类型的数据都有,既有内部叶节点,又有叶子结点。这也就意味着碎片区里的数据属于不同的段

所以,每个段中一开始的一些数据,严格来说是前32个页面,都是来源于碎片区的。之后如果还有数据,则会实际分配一个又一个的区域区存储他们。

到这里,我们介绍了碎片区的概念,才能往下介绍区的类别。

区的分类

区可以被分为以下几类:

  • 空闲区:完全崭新的区域,好像白纸。
  • 有剩余空间的碎片区:掺杂着数据的碎片区,新的数据会插入到这里的页面中。
  • 无剩余空间的碎片区:都满了。
  • 附属于某个段的区:就是普通的区,里面的页面都属于一个段。比较“专一”

四种类别也叫四种state,对应英文名FREE、FREE_FRAG、FULL_FRAG、 FSEG

这些状态存储在哪里呢?存储在表示区域元信息的结构体里,这种结构体叫XDES ENTRY。每个结构体都对应一个区。
MySQL是怎样运行的——第九章
这些字段的作用如下:

  • SegmentID:表示这个区所属段的编号。如果该区不属于某个段则这个编号没意义。碎片区就不属于什么段对吧?
  • ListNode:这个部分是链表。可以把自己和兄弟们连起来。
  • State:上边介绍过了,主要是说明区的状态
  • PageStateBitmap:一共128位,每个页占用2位。第一位表示页是否空闲,第二位还没用到呢。

额外补充一下ListNode结构。这个结构细分来看是两组属性,每一组都有一个页号和页内偏移。通过页号和页内偏移可以唯一的定位一个XDES Entry。

XDES链表

上文中我们提到了XDES链表,为什么需要这样的结构呢?
想象这样一个场景,你想要新增一条记录到碎片区。如果有没用完的碎片区,可以直接把数据存到那个碎片区的页里。如果都用完了呢?你需要申请一个崭新的碎片区,然后把数据存到其中的某个页里。在这过程中你还要修改对应的区的状态。

可是我们怎么知道哪些区是有空位的,哪些区不是呢?如果有几千个区,难道要一个一个遍历找空闲区吗?很显然不能。
因此,InnoDb之父把崭新的区、没用完的碎片区、用完的碎片区各自拿链表穿起来,形成了FREE、FREE_FRAG、FULL_FRAG这三条链表。

然而还有一类问题没解决,就是附属于某个段的区。附属于某个段的区经常需要根据段信息被检索到,因此应该以段号为依据,把它们也串联起来。可是附属于某个段的区也有崭新的、空闲的和已满的,因此每个段都要对应三个链表,分别叫FREE、NOT_FULL、FULL

这里简单整理一下:

每个索引都有叶子结点和内部叶节点。也就是说每个索引至少对应两个段。
假设我们的表里有两个索引,最起码有4个段,全表有4*3+3个链表。

最后有个小问题是如何找到这些链表。想唯一定位一个链表里的xdes entry节点,需要页号和页内偏移。innodb设计者干脆设计了这样的结构:
MySQL是怎样运行的——第九章
如图所示,这种结构叫做List Base Node。里面有链表长和定位链表首尾节点的属性。

段的结构

段是区和零散页面的集合。描述段的元信息也需要一些结构体。我们把这种结构体叫做INODE entry。
MySQL是怎样运行的——第九章

  • 段号:没啥说的,必须有
  • NOT_FULL_N_USED:未用完区的链表中已经使用了多少页面
  • 三个链表的首尾节点:必须有
  • Magic Number:标识这个INODE Entry是否已经被初始化了
  • Fragment Array Entry:之前说过段是区和零散页面的集合,那32个零散页面因为都是碎片区的,所以不在链表里。单独有32个属性记录这些页的页号。

各类页面详细情况

前面的文章中有些信息没怎么介绍,比如XDES ENTRY的链表到底存放在什么地方。下面将继续介绍这些细节。

FSP_HDR

固定是每个独立表空间的第一个页面。它存储了表的元信息以及第一个组里的256个XDES ENTRY。(这里要弄清一件事情,链表只是在逻辑上表示第几个区的entry是哪一类的。比如第1 3 5 7 9个entry是FULL的。但它们在物理上还是连续存储在每一组的第一个页面中的。)
MySQL是怎样运行的——第九章
Header和Trailer已经说很多次了。EmptySpace就是没用的空间。XDES ENTRY占用的那部分空间也介绍过了。剩下的就是File Space Header了。
MySQL是怎样运行的——第九章
下面介绍一下各个字段的用处。

  • SpaceID:一看就懂
  • Not Used:一看就懂
  • Size:当前表空间拥有的页面数
  • FRAG_N_USED:表明在FREE_FRAG中已经使用的页面数量
  • List Base Node for FREE/FREE_FREG/FULL_FRAG List:表空间所拥有的三个链表的首尾节点位置
  • FREE_LIMIT:表空间对应的那个磁盘文件是自动扩容的。自动扩容每次都会扩容不少,这导致大部分表空间内的区都是空闲的。如果把这些区全部加入到表空间级的FREE链表里,那表空间的FREE链表就太长了,而且这样的操作本身就很费时间,更何况通常这些崭新的页也都是在物理上连续的。因此InnoDb之父用FREE_LIMIT字段表示一个页号。这个页号以后的页都是崭新的页。
  • Next Unused Segment ID:下一个没使用的段号。在创建新的段时就不用遍历所有的Inode Entry结构了。
  • Space Flags:表空间级的一些属性,详情如下(现在不用一个一个记,没用)MySQL是怎样运行的——第九章
  • List Base Node for SEG_INODES_FREE/FULL List:每个段对应的inode entry都会存放到INODE类型的页面中。如果一个INODE页面存不下,就需要多个INODE页面,这些INODE页面会形成链表。这两个属性就是记录两种链表的首尾节点的。(两种很好理解吧?一种是满的,一种是不满的)

XDES

这个类型的页面就是除第一组以外其他组存放XDES ENTRY的页面。
MySQL是怎样运行的——第九章
可以看到,没有Space Header那部分。剩下的结构都是一样的。

IBUF_BITMAP

这类页面就是存储一些有关change buffer的信息。change buffer在这本书里基本不会介绍,唯一的一点介绍已经写在前面了,简单来说就是更新操作的缓冲池。

INODE

这类页面是存放INODE ENTRY的。每一组的第三个页面是INODE类型的。
MySQL是怎样运行的——第九章
结构如图所示。文件头、尾、空区间就不再介绍了。
一个INODE页面可以存放85个ENTRY,这个规则在介绍XDES和FSP的时候已经涉及到了。多个INODE页面会形成链表。链表分两种,刚才已经介绍过了,存放在FSP页面的space header里。
上文也提到了,唯一定位一个entry需要靠页号和页内偏移。所以定位前后两个entry需要四个属性,也就是List Node for INODE Page List。

这样在创建段的时候,会先去看空闲inode页面链表,找一页插入新的inode entry。如果没有空闲页面了则需要把崭新的页面变成INODE ENTRY空闲页面。

如何通过段找到对应的INODE

B+树对应两种段,如何通过B+树定位到段的inode结构呢?定位INODE结构需要页号+页内偏移,那这个数据存在哪呢?答案是存到B+树根节点所在页面里。在B+树(也就是Index类型的页面)页面的File Header下面,有一个Page Header字段,里面有PAGE_BTR_SEG_LEAF和PAGE_BTR_SEG_TOP两个子字段,这两个子字段就分别存储了叶子节点和内部叶节点的段头信息(其实还存储了表空间id)

MySQL是怎样运行的——第九章

9.3

这一节主要记录系统表空间。
其实和独立表空间差不多,主要差距在第一个组的前几个页面上。
MySQL是怎样运行的——第九章
可以看出,除了第一组的第一个区有几个页面不一样之外,其他的都差不多。
特有的页面如下所示:
MySQL是怎样运行的——第九章
在这里,我们只简单介绍一下数据字典头部信息。

InnoDB数据字典

MySQL会帮我们保管插入的用户记录。对外来说也许可以看成是记录的池子,但对内来说,插入记录是很复杂的。要校验表信息正确不正确,列正确不正确,对应的索引是哪个表空间的,具体的页面在哪,等等。
这些数据也就是元数据,InnoDB定义了一些表去存储这些元数据。
MySQL是怎样运行的——第九章
MySQL是怎样运行的——第九章
这些记录元信息的系统表叫做数据字典。其中最重要的是TABLES、COLUMNS、INDEXSES、FIELDS这四个系统表。

SYS_TABLES

MySQL是怎样运行的——第九章
这个表有两个索引,分别是NAME为主键建立的聚簇索引和以ID为主键建立的二级索引。

SYS_COLUMNS

MySQL是怎样运行的——第九章
这个表有一个聚簇索引,是以TABLE_ID和POS为主键的聚簇索引。

SYS_INDEXES

MySQL是怎样运行的——第九章

SYS_FIELDS

MySQL是怎样运行的——第九章

Data Dictionary Header页面

有了这四个基本表,就可以获取其他系统表和用户自定义的表的元数据。
那么这四个基本表的元数据去哪找呢?不能套娃,只能硬编码到代码里了。就是我们之前看到的系统表空间中的第七个页面,这个页面会固定的存储这些信息。

MySQL是怎样运行的——第九章
注意这里的段头不是B+树里记录内部叶节点和叶子结点对应的段的INODE信息的段头。我们这个第七页是数据字典段

接下来其实主要就是介绍一下Data Dictionary Header

  • Max Table ID:最大的表id,每次新建表这个数值都加1
  • Max Index ID:每次创建索引这个值就加1
  • Max Row ID:每个表都默认有聚簇索引。如果不定义主键也没有非空的unique列,就会用row id来做聚簇索引。这个值就是记录最大的row id。
  • Max Space ID:每次创建表空间都会把这个值+1
  • Mix ID Low(Unused):还没用
  • Root of …:表示那四张最重要的的系统表的索引的根页面。因为Table表有两个索引,所以加起来是5个root。

最后要注意的是这些表是不能直接访问的,但有些数据又有用,因此在information_shcema里提供了一些以INNODB_SYS开头的表,这些表会读取系统表的信息,给我们查询用。它和系统表多少有些差异,但是够用了。

上一篇:旧笔记整理:锁


下一篇:hitcon2014_stkof