前言
至今已经有4年的JAVA开发经验,今天抽空总结一下自己对InnoDB的底层存储的理解,写的不对的地方希望大家不吝赐教,感谢!!!
InnoDB是什么,有什么用
我们知道,数据库数据存储在硬盘上,而数据的来源是我们的应用程序做了一些写入操作,而真正的数据处理是在内存中进行的,这时候就需要一个“工具”将在内存处理后的数据搬迁到硬盘中,显然,这个工具就可以是InnoDB了,所以InnoDB的定义就是:InnoDB是一个将表中的数据存储到磁盘上的存储引擎。
行格式
介绍InnoDB前,必须先要了解几个基础概念。
行格式:我们都知道,数据库以记录为单位向表中插入数据,这个“记录”在磁盘的存放方式也被叫做“行格式”或者“记录格式”,有以下四种类型,分别是Compact、Redundant、Dynamic和Compressed行格式,我们可以在建表的时候指定行格式,语法如下:
CREATE TABLE ‘表名称’ (列信息)ROW_FORMAT= 行格式名称
变长字段列表:主要用来存储一些可变长度数据类型的实际存储大小,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,如果该可变字段允许存储的最大字节数超过255字节并且真实存储的字节数超过127字节,则使用2个字节,否则使用1个字节;
NULL值列表:用位来表示允许为NULL的列,为1时代表改值为NULL,为0代表改址不为NULL;
记录头信息:记录头信息一共5个字节40位结构如下:
记录的真实数据除了我们自己定义的列的数据以外,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行格式
Dynamic和Compressed行格式和Compact行格式挺
像,只不过在处理行溢出数据时有所不同。Compressed行格式和Dynamic不同的一点是,
Compressed行格式会采用压缩算法对页面进行压缩,以节省空间
数据溢出
CREATE TABLE test_varchar( c VARCHAR(50000) )
然后往这个字段插入50000个字符,会发生什么?
如果这个字段存了50000个字符,由于磁盘的页一般是16K16384个字节,很明显一个页是无法存储完这个字段的数据的,更不用说整条记录了。
对于这种情况,不同的行格式做了不同处理:
在Compact和Redundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的该列的前768个字节的数据,然后把剩余的数据分散存储在几个其他的页
中,记录的真实数据处用20个字节存储指向这些页的地址。这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页;
Dynamic和Compressed行格式,不会在记录的真实数据处存储字段真实数据的前768个字
节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地
址。
索引页格式
说完了行记录的行格式,我们再来说说索引页格式。
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部分:
我们每次增加一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,
当前记录被删除时,则会修改记录头信息中的delete_mask为1,也就是说被删除的记录还
在页中,还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除
它们之后把其他的记录在磁盘上重新排列需要性能消耗。
所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这
个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的
话,可能把这些被删除的记录占用的存储空间覆盖掉
同时我们插入的记录在会记录自己在本页中的位置,写入了记录头信息中heap_no部分。
heap_no值为0和1的记录是InnoDB自动给每个页增加的两个记录,称为伪记录或者虚拟记
录。这两个伪记录一个代表最小记录,一个代表最大记录,这两条存放在页的User
Records部分,他们被单独放在一个称为Infimum + Supremum的部分。
记录头信息中next_record记录了从当前记录的真实数据到下一条记录的真实数据的地址
偏移量。这其实是个链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再
注意的一点是,下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值
由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记
录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是
Supremum记录(也就是最大记录)
我们的记录按照主键从小到大的顺序形成了一个单链表,记录被删除,则从这个链表上摘除。
通过上面的操作,这样使得多条记录在一个页中是按照主键自增的有序排序
Page Directory(解决在页中链表查询问题)
想象一下,如果我们要按照主键ID在页中查找某条记录,应该怎么办?
链表查询方法,根据ID按照Infimum挨个查找,匹配则返回,时间复杂度为O(n)。
InnoDB才不会这么傻,它对这个查询做了优化:
1、将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个
组。
2、每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示
该记录拥有多少条记录,也就是该组内共有几条记录。
3、将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地
方,这个地方就是所谓的Page Directory,也就是页目录页面目录中的这些地址偏移量被
称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
4、每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大
记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能
在是 4~8 条之间。如下图
通过二分查找法,能够尽可能地降低查找时间。
Page Header
InnoDB为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息
官方参考地址:https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html