InnoDB记录存储结构和索引页结构

 InnoDB记录存储结构和索引页结构

InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还

是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内

存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知

道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记

录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?

InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单

位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB

的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行

格式或者记录格式。InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact、

Redundant、Dynamic和Compressed行格式。

行格式

我们可以在创建或修改表的语句中指定行格式:CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称

COMPACT

我们知道MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。如果该可变字段允许存储的最大字节数超过255字节并且真实存储的字节数超过127字节,则使用2个字节,否则使用1个字节。

表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地

方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表。每个允

许存储NULL的列对应一个二进制位,二进制位的值为1时,代表该列的值为NULL。二进制

位的值为0时,代表该列的值不为NULL。

还有一个用于描述记录的记录头信息,它是由固定的5个字节组成。5个字节也就是40个二

进制位,不同的位代表不同的意思。

预留位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 表示下一条记录的相对位置

记录的真实数据除了我们自己定义的列的数据以外,MySQL会为每个记录默认的添加一些

列(也称为隐藏列),包括:

DB_ROW_ID(row_id):非必须,6字节,表示行ID,唯一标识一条记录

DB_TRX_ID:必须,6字节,表示事务ID

DB_ROLL_PTR:必须,7字节,表示回滚指针

InnoDB表对主键的生成策略是:优先使用用户自定义主键作为主键,如果用户没有定义主

键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为

表默认添加一个名为row_id的隐藏列作为主键。 DB_TRX_ID(也可以称为trx_id) 和

DB_ROLL_PTR(也可以称为roll_ptr) 这两个列是必有的,但是 row_id 是可选的(在没

有自定义主键以及Unique键的情况下才会添加该列)。其他的行格式和Compact行格式差别不大。

Dynamic和Compressed行格式

MySQL5.7的默认行格式就是Dynamic,Dynamic和Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有所不同。Compressed行格式和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。

数据溢出

如果我们定义一个表,表中只有一个VARCHAR字段,如下:

CREATE TABLE test_varchar( c VARCHAR(60000) )

然后往这个字段插入60000个字符,会发生什么?

前边说过,MySQL中磁盘和内存交互的基本单位是页,也就是说MySQL是以页为基本单位来管理存储空间的,我们的记录都会被分配到某个页中存储。而一个页的大小一般是16KB,

也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65532个字节,这样就可能造成一个页存放不了一条记录的情况。在Compact行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的该列的前768个字节的数据,然后把剩余的数据分散存储在几个其他的页中,记录的真实数据处用20个字节存储指向这些页的地址。这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页。Dynamic和Compressed行格式,不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址

索引页格式

前边我们简单提了一下页的概念,它是InnoDB管理存储空间的基本单位,一个页的大小一

般是16KB。InnoDB为了不同的目的而设计了许多种不同类型的页,存放我们表中记录的那种类型的页自然也是其中的一员,官方称这种存放记录的页为索引(INDEX)页,不过要理解成数据页也没问题,毕竟存在着聚簇索引这种索引和数据混合的东西。

一个InnoDB数据页的存储空间大致被划分成了7个部分:

File Header 文件头部 38字节 页的一些通用信息

Page Header 页面头部 56字节 数据页专有的一些信息

Infimum + Supremum 最小记录和最大记录 26字节 两个虚拟的行记录

User Records 用户记录 大小不确定 实际存储的行记录内容

Free Space 空闲空间 大小不确定 页中尚未使用的空间

Page Directory 页面目录 大小不确定 页中的某些记录的相对位置

File Trailer 文件尾部 8字节 校验页是否完整

User Records

我们自己存储的记录会按照我们指定的行格式存储到User Records部分。但是在一开始生

成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free

Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records

部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页

使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

当前记录被删除时,则会修改记录头信息中的delete_mask为1,也就是说被删除的记录还

在页中,还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除

它们之后把其他的记录在磁盘上重新排列需要性能消耗。

所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这

个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的

话,可能把这些被删除的记录占用的存储空间覆盖掉。

同时我们插入的记录在会记录自己在本页中的位置,写入了记录头信息中heap_no部分。

heap_no值为0和1的记录是InnoDB自动给每个页增加的两个记录,称为伪记录或者虚拟记

录。这两个伪记录一个代表最小记录,一个代表最大记录,这两条存放在页的User

Records部分,他们被单独放在一个称为Infimum + Supremum的部分。

记录头信息中next_record记录了从当前记录的真实数据到下一条记录的真实数据的地址

偏移量。这其实是个链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再

注意的一点是,下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值

由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记

录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是

Supremum记录(也就是最大记录)

我们的记录按照主键从小到大的顺序形成了一个单链表,记录被删除,则从这个链表上摘

除。

Page Directory

Page Directory主要是解决记录链表的查找问题。如果我们想根据主键值查找页中的某条

记录该咋办?按链表查找的办法:从Infimum记录(最小记录)开始,沿着链表一直往后

找,总会找到或者找不到。但是时间复杂度不低。

InnoDB的改进是,为页中的记录再制作了一个目录,他们的制作过程是这样的:

1.将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个

组。

2.每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示

该记录拥有多少条记录,也就是该组内共有几条记录。

3.将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地

方,这个地方就是所谓的Page Directory,也就是页目录页面目录中的这些地址偏移量被

称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。

4.每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大

记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能

在是 4~8 条之间。如下图:

这样,一个数据页中查找指定主键值的记录的过程分为两步:

  • 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。

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

InnoDB为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记

录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫

Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储

各种状态信息。

File Header

File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说页的类型,这个页的编号是多少,它的上一个页、下一个页是谁,页的校验和等等,这个部分占用固定的38个字节。页的类型,包括Undo日志页、段信息节点、Insert Buffer空闲列表、Insert Buffer位图、系统页、事务系统数据、表空间头部信息、扩展描述页、溢出页、索引页等。同时通过上一个页、下一个页建立一个双向链表把许许多多的页就串联起来,而无需这些页在物理上真正连着。但是并不是所有类型的页都有上一个和下一个页的属性,数据页是有这两个属性的,所以所有的数据页其实是一个双向链表。

File Trailer

我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把

数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间

需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办?

为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),

InnoDB每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2

个小部分:

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

  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN),这个也和校验页的完整性有关。这个File Trailer与File Header类似,都是所有类型的页

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值