絮叨
我们继续来探索mysql。前面我们了解了mysql的索引的一些基础知识,今天我们来康康具体的InnoDB存储引擎
InnoDB页的简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
InnoDB行格式
这个意思就是我们表中一行一行的数据格式,那么这些一行一行的数据格式是怎么样的呢?
设计InnoDB存储引擎的大佬们到现在为止设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式,随着时间的推移,他们可能会设计出更多的行格式,但是不管怎么变,在原理上大体都是相同的。
这边就随便了解一个就好了
COMPACT行格式
从图中可以看出来 这种行格式分为2个部分,第一个部分是记录这一行的额外信息,第二个部分就是记录的真实数据
- 变长字段长度列表
- 我们知道MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把MySQL服务器搞懵,所以这些变长字段占用的存储空间分为两部分:
- 真正的数据内容
- 占用的字节数
- 所以就是像前面说的,如果能确定字符串的长度,尽量使用定长的字段
- NULL值列表
- 我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表中。
- 记录头信息
- 里面就是用来记录当前的行是否被删除,当前行的下一行的记录等等这写信息
- 记录的真实数据
- 除了当前表中字段的真实数据之外,还有很多其他的隐藏数据,我们称之为隐藏列
- row_id 行ID,唯一标识一条记录
- transaction_id 事务ID
- roll_pointer 回滚指针
上面就是我们一行数据里面大概真实存放的东西了。接下来我们来聊聊页,多个行组成的页。
索引(INDEX)页预览
索引页代表的这块16KB大小的存储空间可以被划分为多个部分,不同部分有不同的功能,各个部分如图所示:
User Records
在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下
为了更好的管理在User Records中的这些记录,InnoDB可费了一番力气呢,在哪费力气了呢?不就是把记录按照指定的行格式一条一条摆在User Records部分么?其实这话还得从记录行格式的记录头信息中说起。
User Records 里面存储的就是我们前面说的行格式的数据 下面我们来说说它具体的东西吧
- delete_mask
这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。 当我们下次再插入相同的数据的时候,会复用这个空间
- min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记
- heap_no 这个属性表示当前记录在本页中的位置
- record_type 这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。
- next_record 这玩意儿非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比方说第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。如果你熟悉数据结构的话,就立即明白了,这其实是个链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再注意的一点是,下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录) ,为了更形象的表示一下这个next_record起到的作用,我们用箭头来替代一下next_record中的地址偏移量:
从上面我们可以看出 再页里面的行 其实就是一个链表,方便我们去查询遍历。
Page Directory
这个干嘛的呢,上面我们说了 页里面的行可以做成一个链表去查询,但是要知道一个页16k 可以存储的数据会特别多,所以呢如果只是一个链表的话,那么查询的效率肯定不高,所以mysql 把几个行记录分组一个组,一个组也叫一个槽,然后把这些槽用一个链表连起来,那么下次去查询的时候先去查槽,然后再去查组的这种形式。思想和跳表的思想很像哈哈。
所以在一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Header
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息。
File Header
上边唠叨的Page Header是专门针对数据页记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽了呀。我们现在描述的File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦吧啦吧啦
这个我也稍微提一点 它里面有存 FIL_PAGE_PREV和FIL_PAGE_NEXT,这个是什么意思呢,
我们前边强调过,InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大(比方说一张表中可以有成千上万条记录),InnoDB可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,不过我们唠叨的数据页(也就是类型为FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页其实是一个双链表,就像这样:
File Trailer
我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),设计InnoDB的大佬们在每个页的尾部都加了一个File Trailer部分。
总结
文章内容出自 MySQL 是怎样运行的:从根儿上理解 MySQL,