本文转载自微信公众号「SH的全栈笔记」,作者SH的全栈笔记。转载本文请联系SH的全栈笔记公众号。
想了很久的标题,算了,就这样吧。
这应该是 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.frm 和 student.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_001 和 undo_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 服务器启动的时候创建。