InnoDB 表空间

这应该是 MySQL 原理中最底层的部分了,我们存在 MySQL 中的数据,到底在磁盘上长啥样。你可能会说,数据不都存储在聚簇索引中吗?但很遗憾,你并没有回答我的问题。我会再问你,那聚簇索引在磁盘上又长啥样?

就像 Redis 的 RDB 文件,最终落在磁盘上就是一个真真切切的 dump.rdb 文件,而 MySQL 就显得有点迷,我们只知道通过 SQL 去拿数据,并不知道数据最终是以什么方式进行存储的。当然,了解其底层的存储逻辑,并不仅仅是为了满足好奇心这么简单。

其底层的存储方式,会影响到聚簇索引中数据的存储,进而影响到 MySQL 的 DML(Data Manipulation Language) 性能,所以对底层存储逻辑有个清晰的认知,就能够在某些对性能有着极致追求的场景下,帮助我们对 MySQL 进行优化。

表在磁盘上到底长啥样

首先我们先不扯像表空间这类的专业词汇,让我们先来建一张表,从磁盘的结构上来看一下。首先你得找到 MySQL 的数据目录,如果你是用 Docker 启动的话,这个目录大概长下面这样:

/data00/docker/volumes/ef876f70d5f5c95325c2a79689db79cc4d1cecb7d96e98901256bd49ca359287/_data

然后我们新建一个叫 test 的 DB,然后在 _data 的这个目录下就会多一个 test 的目录。然后在 test 数据库下新建了一张 student 的表,在 test 目录下就会多出两个文件,分别是 student.frmstudent.ibd

可以发现,最终数据在磁盘上的宏观表现其实很简单,就这么些个文件,什么索引啊、页啊都先忽略不管。

对于后缀为 .frm 文件,里面都有啥?里面包含了每张表的元数据,包括了表的结构定义。而 .ibd 文件里则存放了该表的数据索引

我看到有人在博客里把 .ibd 写成了 .idb…虽然 db 看着更顺,但很遗憾并不正确,你把 ibd 的全称 innodb data 记住,就不会把缩写记错了。

上面说的这个以表名命名的 ibd 文件,其实还有一个专业术语叫表空间

顾名思义可以理解为我这个专属的空间

认识表空间

如果我上来就直接告诉你,InnoDB 中有个概念叫表空间,你大概率是很难理解的。

像上文描述的这种每张表都有自己单独的数据存储文件的,叫独占表空间;相对应的,InnoDB 还有自己的系统表空间,在系统表空间下,所有表的数据都存储在同一个文件中。

那数据什么时候存储在系统表空间,又什么时候存储在独占表空间呢?

这个可以通过 MySQL 的配置项 innodb_file_per_table 来决定。当该配置项开启时,每张表都会有自己单独的表空间;相反,当该配置项关闭时,表数据将会存储在系统的表空间内。

该配置项是默认开启的,你可以在 MySQL 中通过命令 SHOW VARIABLES LIKE 'innodb_file_per_table' 来查看该变量的状态

其实从 MySQL 将独占表空间作为默认的设置来看,你就应该知道独占表空间的性能肯定是要比系统表空间好的。

因为对于系统表空间来说,通常只有一个文件,所有的表数据都在这一个文件中,如果我们对某张表进行 TRUNCATE 操作,需要将分散在文件中各个地方的数据删除。首先这样做性能就不好,其次 TRUNCATE 操作会在该文件中产生很多空闲的碎片空间,并且并不会减少共享表空间文件 ibdata1 的大小。

不能理解的话,可以想象 Java 里的标记-清理垃圾回收算法,该算法会在清理的时候造成大量的内存碎片,不利于提高后期的内存利用率

而对于独占表空间来说,从始至终一整张表的数据都只存储在一个文件,比起共享表空间谁更容易清理并且还能释放磁盘空间,简直是一目了然。所以,对于独占表空间来说,TRUNCATE 的性能会更好。

除此之外,独占表空间能够提升单张表的最大容量限制,这块可能不是很好理解,为什么独占表空间还有这个功效?在这里你只需要记住这个结论就好了,后文讲到页相关的东西时,我们会具体的论证。

了解了表空间的概念之后,我们就可以继续深入了解数据在表空间内到底是怎么存储的了。

深入表空间文件内部

其实在很早之前我讲 InnoDB的内存架构 时我就讲过,在 InnoDB 中,页是其数据管理的最小单位。所以讲道理我们应该从其最小的部分开始,但是之前已经专门写过一篇文章来讨论页了,所以在这里就不赘述了。

表空间由一堆的页(Pages)组成,并且每张页的大小是相等的,页大小默认为 16K,当然这个大小可以调整。

页大小可以通过配置项 innodb_page_size 根据业务的实际情况进行调整,可以选择的大小分别为 4K、8K、16K、32K和64K

一堆页组合在一起,就变成了区(Extents)

每个的大小是固定的。当我们设置了不同的 innodb_page_size 时,每个区(Extents)内所包含的页的数量、和对应的固定区大小都不同,具体的情况如下图所示。

innodb_page_size 为 4K、8K或者16K时,其对应的区(Extents)大小为1M;当其页大小为32K时,区大小为2M;当页大小为64K时,区大小为4M。

MySQL 5.6的时候其实只支持 4K、8K和16K,至于上面说到的32K和64K,是在 MySQL 5.7.6 之后添加的。

随着页和区大小的变动,每个区内所能容纳的 页数量 也会随之改变。举个例子,当 innodb_page_size 的值为 16K 时,每个区就包含 64 页;当其为 8K 时,每个区包含 128 页;当其为 4K 时,每个区就会包含 256 页。

上面聊过,一页一页的数据组成了,而一个一个区则组成了段(Segments)

逻辑上,InnoDB 的表空间就是由一个一个这样的段(Segment)组成的。随着数据量的持续增长需要申请新的空间时,InnoDB 会先请求32个页,之后便会直接分配一整个区(Extents) 。甚至在某个很大的 Segment 内,还会一次性分配 4 个区。

默认情况下,InnoDB 会给每个索引分配两个段(Segment)。一个用于存储索引中的非叶子结点,另一个用于存储叶子结点。

表空间的分类

上面大概介绍了两种表空间类别,分别是系统表空间、独占表空间。接下来就需要详细的了解一下各个表空间分类的细节了。

系统表空间

当我们开启了innodb_file_per_table 这个配置项(默认就是开启的)之后,系统表空间内就只用于存储 Change Buffer 相关的数据。而当我们将其关闭之后,系统表空间内就会存储表和索引相关的数据。当然,在 MySQL 8.0之前,独占表空间内还包含了 Double Write Buffer(两次写缓冲),但在 MySQL 8.0.20 之后被移了出去,存放在了一个单独的文件中。

默认情况下,系统表空间只会有一个叫 ibdata1 的数据文件,当然,它是允许有多个文件存在的。这所有的属性包括文件名称、文件大小都是通过配置项目 innodb_data_file_path 来制定的,举个例子:

innodb_data_file_path=ibdata1:10M:autoextend

这里指明了系统表空间的文件名为ibdata1 ,初始化大小为10M 。你可发现了,这个 autoextend 是个什么鬼?

刚刚说到,初始大小是 10M ,那么随着 MySQL 的运行,其数据量会慢慢的增长,数据文件必须要申请更多的空间来存储数据。而定义了 autoextend InnoDB 就会帮我们自动对数据文件进行扩容,每次扩容申请 8M 的空间。当然,这个 8M 也是可以配置的,我们可以通过配置项 innodb_autoextend_increment 来配置。

独占表空间

这块其实上面在引入的时候已经介绍的差不多了,这里简单的总结一下就好。当配置项 innodb_file_per_table 开启时(现在是默认开启的),每张表的数据都会存储自己单独的数据文件中。

常规表空间

这个暂时不用了解,知道常规表空间跟系统表空间类似,也是一个共享的存储空间就好。

Undo 表空间

这里主要存储 Undo Logs,有了 Undo Logs 我们就可以在事务出错之后快速的将更改回滚。InnoDB 会默认给 Undo 表空间创建两个数据文件,如果没有特别指定,其文件名默认为 undo_001undo_002

至于这两个数据文件的具体存放路径,可以通过配置项 innodb_undo_directory 来指定。当然,如果没有指定,Undo 表空间的数据文件就会放在 InnoDB 的默认数据目录下,通常来说是 /usr/local/mysql

而这两个 Undo 表空间数据文件的初始大小,在 MySQL 8.0.23 之前是由 InnoDB 的页大小来决定的,具体的情况如下图:

而在 MySQL 8.0.23 之后,Undo 表空间的初始化大小都是 16M 了。至于 Undo 表空间的扩容,不同的版本也有不通的处理方式。

在 MySQL 8.0.23 之前,每次扩容是申请 4 个区(Extends),按照之前的讨论,如果页大小为 16 K,那么对应到区就是 1M,换句话说,每次扩容申请 4M 的空间,当然这个具体的大小会根据页大小的变化而变化,这个在上文提到过在此就不再赘述。

而在 MySQL 8.0.23 之后,每次最少都要扩容 16 M的空间。而且,为了防止数据量爆发式的增长,InnoDB 对扩容的容量会做一个动态的调整。

如果本次扩容和上次扩容时间差小于 0.1 秒,则扩容的空间会加倍,也就是变成 32 M。如果多次扩容的时间差都小于 0.1 秒,这个 加倍 的操作会 累加,直到达到上限 256M

那你可能会说,那如果某段时间刚好请求量比较大,使得扩容的容量达到了最大的 256 M,那后续请求量下去了呢?难道还是申请 256 M吗?这显的不太合理。所以 InnoDB 判断如果两次扩容间隔大于 0.1 秒,就会将扩容的容量减半,直到减少到最小限制 16 M。

临时表空间

临时表空间内的数据,顾名思义都是临时的。

你在说屁话…

它分为两个部分,分别是:

  • Session 临时表空间
  • 全局临时表空间

对于 Session 临时表空间,里面会存储由用户或者优化器创建的临时表。对于每个 Session 来说,InnoDB 最多会分配两个数据文件(表空间),分别用于存储用户创建的临时表和优化器创建的内部临时表。当 Session 失效之后,这些已分配的数据文件会被 Truncate 然后放到一个 数据文件池 中。

这个操作其实跟其他的池化技术没有区别,值得注意的是,这些文件被 Truncate 了之后大小并不会发生变化。而这个数据文件池会在 MySQL 服务器启动的时候创建,里面会默认扔进去 10 个文件,每个文件的默认大小为 5 页。

而对于全局临时表空间,里面会存对临时表做了改动的回滚段(Rollback Segment),其初始化的大小大约是 12 M,同样会在 MySQL 服务器启动的时候创建。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 注销 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s