
作为mysql5.5之后的默认存储引擎,同时也是我们在开发中大量使用的存储引擎,非常有必要专门开一个章节,去探究Innodb的底层。
innodb的存储格式从行,页,区,段,再到表空间,环环相扣。接下来就介绍存储格式

行格式
COMPACT行格式

一条完整的记录行,包括额外信息
和真实数据
两部分,接下来就相继介绍这些部分。
额外信息
:
变长字段长度列表:
MySQL 支持一些变长的数据类型,比如 VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类型,这些数据类型的列称为 变长字段 ,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

这些长度的描述是逆序存放的,这样一条记录在读取真实数据的时候向右读取到的第一列,同时再向左读取其长度。比如 列1长度04,列2长度03,这样逆序排列。
值的注意的是,null值字段的长度是不存储的。
null值列表:
我们常常说,建议字段设置成非空,这是因为如果字段可为空的话,mysql需要额外开辟一个字节来存字段是否为空的信息,也就是null值列表。
但是实际上字段是否为null的存储其实很小的啦。一个字节有8位,就能描述8个可空的字段了,也是通过逆序的方式,每个位都可以表示一个字段,0为非空1为空。

假设c3和c4字段都为空,那么效果就是上图所示。如果一行数据可空字段超过了8个,那么就要用2字节来描述了。
记录头信息:
记录头信息由固定的5个字节组成,也就是40位。

我们只需要关心几个常见字段,
delete_mask
1bit 标记该记录是否被删除 (底层记录删除只是修改该状态,这样对mvcc才是可见的)
min_rec_mask
1bit B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned
4bit 表示当前记录拥有的记录数,这个在页中的每个槽的最后记录都会记录该值
record_type
3bit 表示当前记录的类型, 0 表示普通记录, 1 表示B+树非叶子节点记录, 2 表示最小记录, 3 表示最大记录
next_record
16bit 表示下一条记录的相对位置**偏移量**
(存储形成单向链表,才提高了范围查询的速度)
真实数据
:
对于字段来说,除了我们自己定义的字段, MySQL 会为每个记录默认的添加一些列(也称为 隐藏列 ),具体的列如下:

InnoDB存储引擎会为每条记录都添加 transaction_id
和 roll_pointer
这两个列,但是 row_id
是可选的(在没有自定义主键以及Unique键的情况下才会添加该列) 这时候row_id就变成了我们的聚簇索引。这些字段在后续的mvcc中提供了巨大的作用。
行溢出
一般情况下一个页是16k,如果我们的字段设置了很多,同时字段的长度又很大,就会出现一个页都存放不了一行的尴尬情况,称为行溢出。在compact行格式下,针对行溢出,只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中。

Dynamic行格式
Dynamic行格式是5.7默认的行格式,他和compact的差别在于对行溢出的处理。dynamic格式会将溢出字段的所有数据都放在别的页,字段只存该页地址。

页格式
页是InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB 。InnoDB 为了不同的目的而设计了许多种不同类型的 页 ,比如存放表空间头部信息的页,存放 Insert Buffer 信息的页,存放 INODE 信息的页,存放 undo 日志信息的页等等等等。我们聚焦的是那些 存放我们表中记录的那种类型的页,官方称这种存放记录的页为索引( INDEX )页。
一个16kb的页又被分为多个部分,不同的部分提供不同的功能。

其中,大小不确定的user records
就是用来存我们的行记录的,而随着记录增加userrecords
越来越大,free space
越来越小。
像这样:

Infimum + Supremum
每个页都会有一条最小记录,和最大记录。这是用来辅助我们定位的,他们起到了关键的作用,比如我们的临键锁,需要锁(100,+无穷)的位置,就是靠在最后一条suprenum记录上加锁的。
它们拥有和普通记录一样的记录头,具体如下:

具体的作用呢,就是将我们页内的所有记录给串联起来,统一管理,这里就用到了记录头的next-record
指针

这样我们的记录就从小到大形成了一个单向链表,方便了之后的检索。如果现在需要删除一条记录2,会怎么做呢?

它做了几件事:
第2条记录并没有从存储空间中移除,而是把该条记录的 delete_mask
值设置为 1第2条记录的 next_record
值变为了0,意味着该记录没有下一条记录了第1条记录的 next_record
指向了第3条记录还有一点你可能忽略了,就是 最大记录 的 n_owned
值从 5 变成了 4
上面的这些记录头,都是行格式介绍的,现在知道具体用处了吧,如果忘了?滚回去再学一遍!
这里注意一点,下一条记录的指针指向的都是记录头和真实数据中间的那个节点。这就是为啥null值和变长字段都是逆序排放的了,从中间向两边散开,分别找到对应一个字段的情况就最快。
Page Directory(页目录)
了解到了页存储数据的格式是个单向链表,那你会想到,单向链表的查询复杂度不是O(n)吗,那不是贼慢,不是说b+树么?还没到b+树,别着急。先了解页内部的查询,也就是本节的主角,页目录。
为了提高页内的查询速度,我们采用了一个槽来对记录行进行分组

InnoDB对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录, 最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
所以当记录数足够多的时候是这样的:

有了很多个槽,这些槽形成了页目录。查询一条记录的时候,我们可以先通过二分法,找到记录对应的槽,然后再从槽的最小记录开始,通过单向链表遍历,不超过8行记录,就能找到我们想要的记录了。
Page Header(页面头部)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录
,第 一条记录的地址是什么,页目录中存储了多少个槽
等等,特意在页中定义了一个叫 Page Header 的部分
File Header(文件头部)
File Header 针对各种类型的页都通用,也就是说不同类型的页都会以 File Header 作 为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、 下一个页是谁等问题。我们的页在叶子节点中双向链表相连,靠的就是这个。

File Trailer
为了加快速度,页都会被加载进buffer pool缓存中修改,然后再以一定的频率刷新回磁盘,那如果刷新一半就断电了怎么办?
为此,页头部和页尾部都存放了当前页的校验和,如果头尾的校验和不一致,说明刷盘中途出现了问题。
该部位分为八个字节,两个部分
前一个部分4个字节,存校验和
后一部分4个字节,存该也刷盘的时候对应的redolog中的LSN(后面会提)。
区格式
由于页实在是太多了,为了更好的管理,mysql提出了区的概念。连续的64个页
就是一个区
也就是区默认1MB
的空间。这样就能够实现大多的页在申请的时候都是连续的了,会更好的加快我们查询寻址的数据,也是提高了我们的范围检索速度,减少随机io
。
段格式
我们想要的顺序检索,实际上是叶子节点上的顺序,所以mysql又提出了段的概念,分为了索引段和数据段,做更好的隔离。
常见的段有:
数据段:B+树的叶节点。
索引段:B+树的非叶节点。
回滚段:即rollback segment,管理undo log segment。
参考资料
《mysql是怎样运行的》

END
后台回复关键词 mysql 获取今日推荐资料
微信8.0新增了一万的好友数,之前没加上好友的可以加一下我的个人微信,再晚又满了,一起抱团取暖,结伴内卷。

扫码拉群,学习打卡,交流经验
每周一读




