一篇学会 InnoDB 表空间

网络 通信技术
我们存在 MySQL 中的数据,到底在磁盘上长啥样。你可能会说,数据不都存储在聚簇索引中吗?但很遗憾,你并没有回答我的问题。我会再问你,那聚簇索引在磁盘上又长啥样?

[[408831]]

本文转载自微信公众号「SH的全栈笔记」,作者SH的全栈笔记。转载本文请联系SH的全栈笔记公众号。

想了很久的标题,算了,就这样吧。

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

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

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

表在磁盘上到底长啥样

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

  1. /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 来制定的,举个例子:

  1. 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 服务器启动的时候创建。

 

责任编辑:武晓燕 来源: SH的全栈笔记
相关推荐

2021-09-07 17:54:04

OpenGauss分区表索引

2022-06-22 07:32:53

Sharding分库数据源

2022-08-29 08:00:11

哈希表数组存储桶

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2021-05-11 08:54:59

建造者模式设计

2021-07-06 08:59:18

抽象工厂模式

2023-01-03 08:31:54

Spring读取器配置

2021-07-05 22:11:38

MySQL体系架构

2023-11-28 08:29:31

Rust内存布局

2022-08-26 09:29:01

Kubernetes策略Master

2022-08-23 08:00:59

磁盘性能网络

2022-04-12 08:30:52

回调函数代码调试

2021-10-27 09:59:35

存储

2021-07-02 08:51:29

源码参数Thread

2021-07-16 22:43:10

Go并发Golang

2023-03-13 21:38:08

TCP数据IP地址

2021-09-28 08:59:30

复原IP地址

2023-11-01 09:07:01

Spring装配源码

2021-10-14 10:22:19

逃逸JVM性能
点赞
收藏

51CTO技术栈公众号