我们知道 InnoDB 存储引擎是把表存储在磁盘上的,而操作系统又通过文件系统来管理磁盘。当我们想读取数据时,存储引擎会从文件系统中把数据读出来返回给我们,当我们想写入数据时,存储引擎会把这些数据又写回文件系统。下面我们就分析一下 InnoDB 存储引擎的数据是如何在文件系统中存储的。
MySQL 数据目录
MySQL 服务器程序在启动时会到文件系统的某个目录下加载一些文件,之后在运行过程中产生的数据也都会存储到这个目录下的某些文件中,这个目录就称为数据目录。数据目录路径可通过系统变量 datadir 指定,在客户端中也可以进行查看:
MySQL 在运行过程中都会产生哪些数据呢?当然会包含我们创建的数据库、表、视图和触发器等用户数据,除了这些用户数据,为了程序更好的运行,MySQL 也会创建一些其他的额外数据,下来详细分析下这个数据目录下的内容。
1. 数据库
每个数据库都对应数据目录下的一个子目录,或者说对应一个文件夹。每当我们使用 CREATE DATABASE 语句创建一个数据库的时候,MySQL 都会执行以下两步:
- 在数据目录下创建一个和数据库名同名的子目录。
- 在该与数据库名同名的子目录下创建一个名为 db.opt 的文件,这个文件中包含了该数据库的各种属性,比如该数据库的字符集和比较规则等。
我们先查看一下当前有哪些数据库:
可以看到,除了 information_schema 这个系统数据库外,其他的数据库在数据目录下都有对应的子目录。因为这个 information_schema 比较特殊,MySQL 对它的实现进行了特殊对待,没有使用相应的数据库目录。我们以 user 数据库为例,查看该目录下的内容:
果然,db.opt 文件中保存了数据库的字符集与比较规则。
2. 表
我们的数据其实都是以记录的形式插入到表中的,每个表的信息其实可以分为两种:
- 表结构的定义
- 表中的数据
表结构就是该表的名称是啥,表里边有多少列,每个列的数据类型是啥,有啥约束条件和索引,用的是啥字符集和比较规则等各种信息,这些信息都体现在了我们的建表语句中了。为了保存这些信息,InnoDB 存储引擎在数据目录下对应的数据库子目录下创建了一个专门用于描述表结构的文件,文件名为:表名.frm。注意,这个后缀名为 .frm 的文件是以二进制格式存储的,直接打开是会乱码的。
InnoDB 是如何存储表数据的?
我们知道,InnoDB 其实是使用页为基本单位来管理存储空间的,默认的页大小为 16KB。对于 InnoDB 存储引擎来说,每个索引都对应着一棵 B+ 树,该 B+ 树的每个节点都是一个数据页,数据页之间不必要是物理连续的,因为数据页之间有双向链表来维护着这些页的顺序。InnoDB 的聚簇索引的叶子节点存储了完整的用户记录,也就是所谓的索引即数据,数据即索引。
为了更好的管理这些页,InnoDB 提出了表空间(table space)的概念,这个表空间是一个抽象的概念,它可以对应文件系统上一个或多个真实文件。每一个表空间可以被划分为很多个页,我们的表数据就存放在某个表空间下的某些页里。当我们想为某个表插入一条记录的时候,就从表空间中捞出一个对应的页来写入数据。InnoDB 将表空间划分为如下几种不同的类型:
系统表空间(system tablespace)
这个所谓的系统表空间可以对应文件系统上一个或多个实际的文件,默认情况下,InnoDB 会在数据目录下创建一个名为 ibdata1、大小为 12M 的文件,这个文件就是对应的系统表空间在文件系统上的表示。这个文件是所谓的自扩展文件,也就是当不够用的时候它会自己增加文件大小。需要注意的是,在一个 MySQL 服务器中,系统表空间只有一份。从 MySQL 5.5.7 到 MySQL 5.6.6 之间的各个版本中,我们表中的数据都会被默认存储到这个系统表空间中。
独立表空间(file-per-table tablespace)
在 MySQL 5.6.6 以及之后的版本中,InnoDB 并不会默认的把各个表的数据存储到系统表空间中,而是为每一个表建立一个独立表空间,也就是说我们创建了多少个表,就有多少个独立表空间。使用独立表空间来存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表名相同,只不过添加了一个 .ibd 的扩展名而已,所以完整的文件名称为:表名.ibd。
当然我们也可以自己指定使用系统表空间还是独立表空间来存储数据,这个功能由启动参数 innodb_file_per_table 控制,如果我们想刻意将表数据都存储到系统表空间时,可以在启动 MySQL 服务器的时候这样配置:
[server]
innodb_file_per_table=0
当 innodb_file_per_table 的值为 0 时,代表使用系统表空间;当 innodb_file_per_table 的值为 1 时,代表使用独立表空间。不过 innodb_file_per_table 参数只对新建的表起作用,对已经分配了表空间的表不起作用。
3. 视图
我们知道 MySQL 中的视图其实是虚拟的表,也就是某个查询语句的一个别名而已,所以在存储视图的时候是不需要存储真实的数据的,只需要把它的结构存储起来就行了。和表一样,描述视图结构的文件也会被存储到所属数据库对应的子目录下边,只会存储一个 视图名.frm 的文件。
InnoDB 表空间
从 InnoDB 存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,我们称之为表空间(table space)。表空间又由段(segment)、区(extent)、页(page)组成。
InnoDB 提供了两种表空间存储数据的方式,一种是共享表空间,一种是独占表空间。我们可以通过设置 innodb_file_per_table 参数为 1(1 代表独占方式)开启独占表空间模式。开启之后,每个表都有自己独立的表空间物理文件,所有的数据以及索引都会存储在该文件中,这样方便备份以及恢复数据。
表空间中的每一个页都对应着一个页号,也就是 FIL_PAGE_OFFSET,这个页号由 4 个字节组成,也就是 32 个比特位,所以一个表空间最多可以拥有 232 个页,如果按照页的默认大小 16KB 来算,一个表空间最多可以支持 64TB 的数据。表空间的第一个页的页号为 0,之后的页号分别是 1、2、3… 依此类推。
1. 独立表空间结构
1.1 区
为了更好的管理表空间中的页,InnoDB 提出了区(extent)的概念。对于 16KB 的页来说,连续的 64 个页就是一个区,也就是说一个区默认占用 1MB 空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每 256 个区被划分成一组。具体如下图所示:
理论上来说,不引入区的概念只使用页的概念对存储引擎的运行并没啥影响,那为什么要引入区的概念呢?我们知道,表中的记录是存储在数据页里的,数据页作为节点组成了 B+ 树,这棵 B+ 树就是聚簇索引。如果我们表中数据量很少的话,的确用不到区的概念,因为简单的几个页就能把对应的数据存储起来,但如果表里的记录越来越多时,考虑下边这个场景:
我们每向表中插入一条记录,本质就是向该表的聚簇索引及所有二级索引代表的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。这样在进行范围查询时,沿着双向链表扫描时,如果链表中相邻的两个页的物理位置离得非常远,这个查询过程就是所谓的随机 I/O。由于磁盘的随机 I/O 是非常慢的,所以我们要尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询时才可以使用所谓的顺序 I/O。
因此,才引入了区(extent)的概念,一个区就是在物理位置上连续的 64 个页。在表中数据量大的时候,为某个索引分配空间时就不再按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据特别多时,可以一次性分配多个连续的区。虽然会造成空间浪费,但从性能角度看,可以消除很多的随机 I/O,功大于过。
1.2 段
上面我们提到的范围查询,其实是对 B+ 树的叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以 InnoDB 对 B+ 树的叶子节点和非叶子节点进行了区分,即叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(Segment),存放非叶子节点的区的集合也算是一个段。也就是说,一个索引会生成两个段,一个叶子节点段,一个非叶子节点段。
默认情况下,一个使用 InnoDB 存储引擎的表只有一个聚簇索引,一个索引会生成 2 个段,而段是以区为单位申请存储空间的,一个区默认占用 1M 存储空间,所以默认情况下一个只存了几条记录的小表也需要 2M 的存储空间吗?而且以后每次添加一个索引都要多申请 2M 的存储空间吗?
为了解决这个问题,InnoDB 提出了一个碎片(fragment)区的概念,也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段 A,有些页用于段 B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。所以此后为某个段分配存储空间的策略是这样的:
- 在刚开始向表中插入数据时,段是从某个碎片区以单个页为单位来分配存储空间的。
- 当某个段已经占用了 32 个碎片区的页之后,就会以完整的区为单位来分配存储空间。
所以,段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。除了索引的叶子节点段和非叶子节点段外,InnoDB 中还有为存储一些特殊的数据而定义的段,比如回滚段等。
2. 系统表空间结构
系统表空间的结构和独立表空间基本类似,只不过由于整个 MySQL 进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面。因为这个系统表空间最牛逼,相当于是表空间之首,所以它的表空间 ID(Space ID)是 0。