InnoDB 的体系结构组要分为两个部分,分别是内存结构和磁盘结构,如下图:
image.png中·
可以看见,比较关键的是其中的各种 Buffer 和 Tabelspace(表空间),这篇文章我们就来深入了解一下 InnoDB 中的表空间。

什么是表空间?

表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间(File-Per-Table Tablespaces)来说,对应着文件系统中一个名为表名 .ibd 的实际文件。

比如我们通过 “show tables” 命令可以查看某个数据库中的表有哪些:
image.png
接着我们进入到 mysql 的 data/mysqldev 目录中,可以发现,我们在客户端中看到的表在该目录下都有一个对应的 .ibd 文件。
image.png

大家可以把表空间想象成被切分为许许多多个页的池子,当我们想为某个表插入一条记录的时候,就从池子中捞出一个对应的页来把数据写进去。再回忆一次,InnoDB 是以页为单位管理存储空间的。我们的聚簇索引(也就是完整的表数据)和其他的二级索引都是以B+ 树的形式保存到表空间的,而 B+ 树的节点就是数据页。

站在数据结构的角度,我们的数据是以 B+ 数的形式来存储的;但是站在磁盘文件管理的角度,mysql 是以表空间的形式去存储的。任何类型的页都有专门的地方保存页属于哪个表空间。同时表空间中的每一个页都对应着一个页号,也叫表空间中页的偏移量(页号存储在 File Header 中的 FIL_PAGE_OFFSET),这个页号由 4 个字节组成,也就是 32 个比特位,所以一个表空间最多可以拥有 2^32 个页,如果按照页的默认大小 16KB 来算,一个表空间最多支持 64TB 的数据

面试题:请问一个表最多可以存放多少数据?

独立表空间

独立表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在独立表空间中。如果用户启用了参数 innodb_file_per_table,则每张表内的数据可以单独存放到一个表空间内(对应一个 .ibd 文件)。
image.png
可以看出,表空间里面由段、区、页组成。

区(extent)

区是由连续的页构成的,表空间中的页可以达到 2^32 个页,实在是太多了,为了更好的管理这些页面,InnoDB 中还有一个区(英文名:extent)的概念。对于 16KB 的页来说,连续的 64 个页就是一个区,也就是说一个区默认占用 1MB 空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每 256 个区又被划分成一个

第一个组最开始的 3个 页面的类型是固定的:用来登记整个表空间的一些整体属性以及本组所有的区被称为 FSP_HDR,也就是extent 0 ~ extent 255 这 256 个区,整个表空间只有一个 FSP_HDR。

其余各组最开始的 2 个页面的类型是固定的,一个 XDES 类型,用来登记本组 256 个区的属性,FSP_HDR 类型的页面其实和 XDES 类型的页面的作用类似,只不过 FSP_HDR 类型的页面还会额外存储一些表空间的属性。

引入区的主要目的是什么?我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。

当范围查询能有用到索引时,范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机 I/O。随机 I/O 是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序 I/O

一个区就是在物理位置上连续的 64 个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据十分非常特别多的时候,可以一次性分配多个连续的区,从性能角度,可以消除很多的随机 I/O。

段(segment)

我们提到的范围查询,其实是对 B+ 树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以 InnoDB 对 B+ 树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成 2 个段,一个叶子节点段,一个非叶子节点段。段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念。

通过对表空间结构的了解,现在回想一下为什么我们不建议用 uuid 作为主键?表面上看 MySQL 只是维护了一棵 B+ 树,而实际上 MySQL 除了 B+ 树要维护以外,还要对表空间的其他结构进行维护,远远比我们想象中的要复杂。

除了叶子节点段和非叶子节点段之外,还有一个比较常见的回滚段(undo segment),这里不做具体介绍,在后面讲到事务的时候会具体分析。

系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个 MySQL 进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面,相当于是表空间之首,所以它的表空间 ID(Space ID)是0。系统表空间有 extent 1 和 extent 两个区,也就是页号从 64~191 这128个页面被称为Doublewrite buffer,也就是双写缓冲区。

双写缓冲区/双写机制

双写缓冲区/双写机制是 InnoDB 的三大特性之一,作用是为了保证我们所写入数据的可靠性。还有两个是 Buffer Pool、自适应 Hash 索引,这些我另外的文章会讲到。

为什么要引入一个双写机制呢?InnoDB 页的大小一般是16KB,其数据校验也是针对这16KB 来计算的,将数据写入到磁盘是以页为单位进行操作的。而操作系统写文件是以 4KB 作为单位的,那么每写一个 InnoDB 的页到磁盘上,操作系统需要写4个块。而计算机硬件和操作系统,在极端情况下(比如断电)往往并不能保证这一操作的原子性,16K的数据,写入4K时,发生了系统断电或系统崩溃,只有一部分写是成功的,这种情况下会产生 partial page write(部分页写入)问题。这时页数据出现不一样的情形,从而形成一个”断裂”的页,使数据产生混乱。

在 InnoDB 存储引擎未使用 doublewrite 技术前,曾经出现过因为部分写失效而导致数据丢失的情况。