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

摘要

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

InnoDB是一个将表中的数据存储到磁盘上的存储引擎,而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。

而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?
InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。
InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式,MySQL5.7以后的默认行格式是Dynamic。

我们可以在创建或修改表的语句中指定行格式: CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
查看行格式SHOW TABLE STATUS LIKE 'table_name'\G

记录行格式

  • Redundant: MySQL5.0之前用的一种行格式,不予深究

  • Compact: MySQL 5.1 开始,默认的行记录格式为 Compact

    • 变长字段长度列表: 存储当前记录中所有变成字段的长度
      • VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,拥有这些数据类型的字段称为变长字段,变长字段中存储多少字节的数据是不固定的,所以在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。
      • 如果该可变字段允许存储的最大字节数超过255字节并且真实存储的字节数超过127字节,则使用2个字节,否则使用1个字节。
    • NULL值列表: 记录当前记录中允许值为NULL的字段是否为NULL
      • 表中的某些列可能存储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 表示下一条记录的相对位置
    • DB_ROW_ID(row_id):非必须,6字节,表示行ID,唯一标识一条记录(在没有自定义主键以及Unique键的情况下才会添加该列)
    • DB_TRX_ID:必须,6字节,表示事务ID
    • DB_ROLL_PTR:必须,7字节,表示回滚指针,指向记录对应的 undo 日志位置
  • Dynamic: MySQL5.7以后的默认行格式,和Compact行格式挺像,只不过在处理行溢出数据时有所不同

  • Compressed: 与Dynamic很像,不同的一点是Compressed行格式会采用压缩算法对页面进行压缩,以节省空间

数据溢出

如果我们定义一个表,表中只有一个VARCHAR字段,如下: CREATE TABLE test_varchar( c VARCHAR(60000) ) 然后往这个字段插入60000个字符,会发生什么?

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

而一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65532个字节,这样就可能造成一个页存放不了一条记录的情况。

在Compact和Redundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的该列的前768个字节的数据,然后把剩余的数据分散存储在几个其他的页中,

记录的真实数据处用20个字节存储指向这些页的地址。这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页。
Dynamic和Compressed行格式,不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。

索引页格式

  • Mysql把存放记录的页称为索引页,也可以理解为是数据页。

  • 一个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属性遍历该槽所在的组中的各个记录。
  • Page Header

    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类似,都是所有类型的页通用的。

InnoDB表空间(TableSpace)

  • 系统表空间(System Tablespace): 对应着文件系统中一个或多个实际文件,一般是ibdata1,表空间 ID(Space ID)是0

  • 独立表空间(File-Per-Table Tablespaces): 对应着文件系统中一个名称为dbname/tablename.ibd的实际文件

  • 临时表空间(Temporary Tablespaces): 对应着文件系统中一个或多个实际文件,一般是ibtmp1#innodb_temp/temp_数字.ibt

  • 通用表空间(General Tablespaces): 允许多个表存储数据的共享表空间

  • Undo Tablespaces: 对应着文件系统中的undo_001undo_002

  • 关于表空间的详细说明可以参考mysql官方文档

可以查看information_schema.INNODB_TABLESPACES获取表空间信息,information_schema.INNODB_DATAFILES获取表空间对应文件系统的文件路径信息

表空间与页的关系

双写缓冲区/双写机制

  • 双写缓冲区/双写机制是InnoDB的三大特性之一,还有两个是Buffer Pool、自适应Hash索引。

  • 它是一种特殊文件flush技术,带给InnoDB存储引擎的是数据页的可靠性。它的作用是,在把页写到数据文件之前,InnoDB先把它们写到一个叫doublewrite buffer(双写缓冲 区)的连续区域内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复。

  • 所以,虽然叫双写缓冲区,但是这个缓冲区不仅在内存中有,更多的是属于MySQL的系统表空间,属于磁盘文件的一部分。

  • 正常的情况下, MySQL写数据页时,会写两遍到磁盘上,第一遍是写到doublewrite buffer,第二遍是写到真正的数据文件中。如果发生了极端情况(断电),InnoDB再次启动后,发现了一个页数据已经损坏,那么此时就可以从doublewrite buffer中进行数据恢复了。

  • doublewrite buffer的作用有两个:

    • 提高innodb把缓存的数据写到硬盘这个过程的安全性;
    • innodb的事务日志不需要包含所有数据的前后映像,而是二进制变化量,这可以节省大量的IO。
  • doublewrite buffer与Redo Log的区别: Redo Log日志中记录的是对页的物理操作,而doublewrite buffer记录的是页面的全量记录

InnoDB元数据表

  • information_schema数据库

mysql8 mysql5 说明
INNODB_TABLES INNODB_SYS_TABLES 整个InnoDB存储引擎中所有的表的信息
INNODB_COLUMNS INNODB_SYS_COLUMNS 整个InnoDB存储引擎中所有的列的信息
INNODB_INDEXES INNODB_SYS_INDEXES 整个InnoDB存储引擎中所有的索引的信息
INNODB_FIELDS INNODB_SYS_FIELDS 整个InnoDB存储引擎中所有的索引对应的列的信息
INNODB_FOREIGN INNODB_SYS_FOREIGN 整个InnoDB存储引擎中所有的外键的信息
INNODB_FOREIGN_COLS INNODB_SYS_FOREIGN_COLS 整个InnoDB存储引擎中所有的外键对应列的信息
INNODB_TABLESPACES INNODB_SYS_TABLESPACES 整个InnoDB存储引擎中所有的表空间信息
INNODB_DATAFILES INNODB_SYS_DATAFILES 整个InnoDB存储引擎中所有的表空间对应文件系统的文件路径信息
INNODB_VIRTUAL INNODB_SYS_VIRTUAL 整个InnoDB存储引擎中所有的虚拟生成列的信息