MySQL InnoDB存储引擎
本文最后更新于:2023年12月4日 晚上
MySQL 存储引擎
存储引擎的概念
MySQL服务器把数据的存储和提取操作都封装到了一个叫存储引擎的模块里。为了实现不同的功能,MySQL提供了各式各样的存储引擎,不同存储引擎管理的表具体的存储结构可能不同,采用的存取算法也可能不同。
为了管理方便,人们把连接管理、查询缓存、语法解析、查询优化这些并不涉及真实数据存储的功能划分为MySQL server的功能,把真实存取数据的功能划分为存储引擎的功能。各种不同的存储引擎向上边的MySQL server层提供统一的调用接口实现。
常用存储引擎
在此之中,最常用的是InnoDB和MyISAM,InnoDB是MySQL的默认存储引擎。
InnoDB存储引擎
简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎。众所周知,磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
InnoDB行格式
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。目前InnoDB存储引擎的有4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式。
Compact行格式
Compact行格式分为,记录的额外信息和记录的真实数据两大部分。
记录的额外信息
记录的额外信息包括:变长字段长度列表、NULL列列表、记录头信息。
变长字段长度列表
变长字段长度列表是一个按照列顺序记录的变长字段的长度列表,每个长度占用1或2个字节,1个字节表示长度小于等于255,2个字节表示长度大于255。
这个列表的作用是:当我们要获取某条记录的某个变长字段时(例如VARCHAR),其存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。通过这样,形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放。
另外需要注意的一点是,变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。
NULL列列表
NULL列列表是一个按照列顺序记录的NULL列的列表,每个NULL列占用1个字节,1个字节表示有NULL值,0个字节表示没有NULL值。
这个列表的作用是:当我们要获取某条记录的某个列时,我们可以通过这个列表快速的定位到该列是否有NULL值。这样可以减少记录的真实数据
存储的空间。
记录头信息
记录头信息是由固定的5个字节组成。5个字节也就是40个二进制位,不同的位代表不同的意思
记录的真实数据
记录的真实数据是由各个列的数据组成的,每个列的数据占用的字节数是固定的,不会因为数据的实际长度而改变。
记录的真实数据除了我们自己定义的列的数据以外,MySQL会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下:
实际记录的格式如下
Redundant行格式
Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:
没有了变长两个字,意味着Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。
多了个偏移两个字,这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
Redundant行格式中的char类型
Compact行格式在CHAR(M)类型时,分变长字符集和定长字符集的情况,而在Redundant行格式中,只要是使用CHAR(M)类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M的乘积。比方说使用utf8字符集的CHAR(10)类型的列占用的真实数据空间始终为30个字节,使用gbk字符集的CHAR(10)类型的列占用的真实数据空间始终为20个字节。由此可以看出来,使用Redundant行格式的CHAR(M)类型的列是不会产生碎片的。
Dynamic和Compressed行格式
这俩行格式和Compact行格式很像,只不过在处理行溢出数据时有点儿分歧,它们不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
InnoDB的索引页结构
页是InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。InnoDB为了不同的目的而设计了许多种不同类型的页,比如存放表空间头部信息的页,存放Insert Buffer信息的页,存放INODE信息的页,存放undo日志信息的页等等。表中记录的那种类型的页叫做索引(INDEX)页。
页的结构
InnoDb主要分为以下七个部分,下面是其相关的介绍。
记录在页中的存储
在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。
mysql的页会在创建的时候生成一个制定大小的Free Space空间;当增加行时候,尚未使用的存储空间中(Free Space)申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了。
记录在页中的创建
这里回到刚开始的,Compact行格式中的记录头信息。
Mysql在创建一个新的页的时候,会自动创建两记录,分别为Infimum Supremum,分别代表最大记录和最小记录。这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成(对应参数)。
它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分。
在创建了这两条记录之后,我们自己的记录就可以被插入到页的User Records部分了。其中,记录会按照主键的顺序插入到页中,如果主键相同,那么会按照主键以外的唯一索引的顺序插入到页中。
我们自己的创建的记录会通过单链表的方式连接起来,这个单链表的头部就是Infimum记录,尾部就是Supremum记录。通过next_record
指针可以找到下一条记录。next_record
值代表与下一条记录的偏移量(实际指向记录头信息和真实数据之间的位置),如果next_record
值为0,那么就代表这是最后一条记录。
因为这个位置对于数据和信息的位置正好,向左读取就是记录头信息,向右读取就是真实数据。
记录在页中的删除
删除一条记录主要有以下几个变化,如图所示:
- 第二条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1。
- 第二条记录的next_record值变为了0,意味着该记录没有下一条记录了。
- 第一条记录的next_record指向了第3条记录。
- 最大记录的n_owned值从5变成了4
主键值为2的记录被我们删掉了,但是存储空间却没有回收,如果我们再次把这条记录插入到表中,会发生什么事呢?
InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。
记录在页中的查找
我们知道记录在页中是以单链表的形式存储的,那么我们如何通过主键值来查找记录呢?
在数据结构课上学过链表的查找。最为简单的查找是顺序查找。从Infimum记录(最小记录)开始,知道招到链表,但是这种方式显而易见,效率很低。
因此InnoDB引入了页目录(Page Directory)机制,通过这个来简化查找。
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
其中,对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 18 条之间,剩下的分组中记录的条数范围只能在是 48 条之间。
通过这样的分组方式,我们就可以通过二分查找的方式来快速定位到我们想要的记录所在的分组,然后再通过顺序查找组的方式快速定位到我们想要的记录。
Page Header(页面头部)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息。
File Header(文件头部)
File Header描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁之类的信息,这个部分占用固定的38个字节
File Trailer
每个页的尾部都加了一个File Trailer部分,用来校验页信息,这个部分由8个字节组成,可以分成2个小部分:
- 前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个File Trailer与File Header类似,都是所有类型的页通用的。
总结
InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页叫做数据页。
InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,所以在一个页中根据主键查找记录是非常快的,分为两步:通过二分法确定该记录所在的槽;通过记录的next_record属性遍历该槽所在的组中的各个记录。
每个数据页的File Header部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表。
为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的LSN值,如果首部和尾部的校验和和LSN值校验不成功的话,就说明同步过程出现了问题。