我们知道mysql常用的引擎有InnoDB和MyISAM,在mysql5.5前默认引擎是MySAM在mysql5.5之后默认引擎是InnoDB
InnoDB记录存储结构
InnoDB页简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。
InnoDB会将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。
InnoDB行格式
主要分为四种格式
分别是Compact、Redundant、Dynamic和Compressed行格式
COMPACT行格式
1、变长字段长度列表
用来记录用户列中长度可变的类型,比如:VARCHAR(M)、VARBINARY(M)、各种TEXT类型,他是按照列的前后***逆序***排列
比如列名和其长度如下:
列名 | 长度(16进制) |
---|---|
列1 | 0x04 |
列2 | 0x03 |
列3 | 0x01 |
那么他的变长字段列表里存放的数据为 0x01 0x03 0x04
2、NULL值列表
我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表中
a)首先统计表中允许存储NULL的列有哪些。
b)如果表中没有允许存储 NULL 的列,则 NULL值列表 也不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列
二进制位的值为1时,代表该列的值为NULL。
二进制位的值为0时,代表该列的值不为NULL。
c)MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。
例如:
共有四列c1、c2、c3、c4,c2是not null的其他三列可以为null
某一行中c3、c4为null,c1其中有值
则在这个列中会存储
3、记录头信息
它是由固定的5个字节组成。5个字节也就是40个二进制位,不同的位代表不同的意思,如图:
名称 | 大小(单位:bit) | 代表意义 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
record_type | 3 | 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 |
next_record | 16 | 表示下一条记录的相对位置 |
其中有几个需要注意的:
- delete_mask 这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。
这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。- record_type 这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,就是目录页的记录类型,2表示最小记录,3表示最大记录。
- next_record 从当前记录的真实数据到下一条记录的真实数据的地址偏移量
记录的真实数据
记录的真实数据除了c1、c2、c3、c4这几个我们自己定义的列的数据以外,MySQL会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
transaction_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)
InnoDB数据页结构
数据页结构的快速浏览
数据页代表的这块16KB大小的存储空间可以被划分为多个部分,不同部分有不同的功能,各个部分如图所示:
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
- Page Directory(页目录)
记录也制作了一个类似的目录,他们的制作过程是这样的:
1、将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
2、每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
3、将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
所以在一个数据页中查找指定主键值的记录的过程分为两步:
1、通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
2、通过记录的next_record属性遍历该槽所在的组中的各个记录。
- Page Header(页面头部)
主要的几部分:
名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE | 2字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用) |
PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
- File Header(文件头部)
上边唠叨的Page Header是专门针对数据页记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽了呀。我们现在描述的File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分
名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum值) |
FIL_PAGE_OFFSET | 4字节 | 页号 |
FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2字节 | 该页的类型 |
FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
1、FIL_PAGE_OFFSET 每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号来可以唯一定位一个页。
2、FIL_PAGE_PREV和FIL_PAGE_NEXT
FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,只有数据页会有(也就是类型为FIL_PAGE_INDEX的页),所以所有的数据页其实是一个双链表,就像这样:
- File Trailer
我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),设计InnoDB的大叔们在每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:
1、前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File
Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File
Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。2、 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个部分也是为了校验页的完整性的,只不过我们目前还没说LSN是个什么意思,所以大家可以先不用管这个属性。