InnoDB 的表空间

48 篇文章 0 订阅

MySQL学习系列


表空间是一个抽象的概念, 对于系统表空间来说, 对应着文件系统中一个或多个实际文件; 对于每个独立表空间来说, 对应着文件系统中一个名为表名.ibd的实际文件。 大家可以把表空间想象成被切分为许许多多个页的池子, 当我们想为某个表插入一条记录的时候, 就从池子中捞出一个对应的页来把数据写进去。

再回忆一次, InnoDB 是以页为单位管理存储空间的, 我们的聚簇索引(也就是完整的表数据) 和其他的二级索引都是以 B+树的形式保存到表空间的, 而B+树的节点就是数据页。

任何类型的页都有 File Header 这个部分, File Header 中专门的地方(FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID) 保存页属于哪个表空间, 同时表空间中的每一个页都对应着一个页号(FIL_PAGE_OFFSET) , 这个页号由 4 个字节组
成, 也就是 32 个比特位, 所以一个表空间最多可以拥有 2³²个页, 如果按照页的默认大小 16KB 来算, 一个表空间最多支持 64TB 的数据。


独立表空间结构

  • 区(extent)

表空间中的页可以达到 2³²个页, 实在是太多了, 为了更好的管理这些页面,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+树的每一层中的页都会形成一个双向链表, 如果是以页为单位来分配存储空间的话, 双向链表相邻的两个页之间的物理位置可能离得非常远。

我们介绍 B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录, 然后沿着双向链表一直扫描就可以了, 而如果链表中相邻的两个页物理位置离得非常远, 就是所谓的随机 I/O。 再一次强调, 磁盘的速度和内存的速度差了好几个数量级, 随机 I/O 是非常慢的, 所以我们应该尽量让链表中相邻的页的物理位置也相邻, 这样进行范围查询的时候才可以使用所谓的顺序 I/O。

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

  • 段(segment)

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

段其实不对应表空间中某一个连续的物理区域, 而是一个逻辑上的概念。
在这里插入图片描述


系统表空间

  • 整体结构

系统表空间的结构和独立表空间基本类似, 只不过由于整个 MySQL 进程只有一个系统表空间, 在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面, 相当于是表空间之首, 所以它的表空间 ID(Space ID) 是 0。

系统表空间和独立表空间的前三个页面的类型是一致的, 只是页号为 3~7的页面是系统表空间特有的。 分别包括:

页号页面类型英文描述描述
3SYSInsert Buffer Header存储 Insert Buffer 的头部信息
4INDEXInsert Buffer Root存储 Insert Buffer 的根页面
5TRX_SYSTransction System事务系统的相关信息
6SYSFirst Rollback Segment第一个回滚段的页面
7SYSData Dictionary Header数据字典头部信息

系统表空间的 extent 1 和 extent 2 这两个区, 也就是页号从 64~191 这 128个页面被称为 Doublewrite buffer, 也就是双写缓冲区。

双写缓冲区/双写机制

双写缓冲区/双写机制是 InnoDB 的三大特性之一, 还有两个是 Buffer Pool、自适应 Hash 索引。

它是一种特殊文件 flush 技术, 带给 InnoDB 存储引擎的是数据页的可靠性。它的作用是, 在把页写到数据文件之前, InnoDB 先把它们写到一个叫 doublewrite buffer(双写缓冲区) 的连续区域内, 在写 doublewrite buffer 完成后, InnoDB 才会把页写到数据文件的适当的位置。 如果在写页的过程中发生意外崩溃, InnoDB在稍后的恢复过程中在 doublewrite buffer 中找到完好的 page 副本用于恢复。

所以, 虽然叫双写缓冲区, 但是这个缓冲区不仅在内存中有, 更多的是属于MySQL 的系统表空间, 属于磁盘文件的一部分。 那为什么要引入一个双写机制呢?

InnoDB 的页大小一般是 16KB, 其数据校验也是针对这 16KB 来计算的, 将数据写入到磁盘是以页为单位进行操作的。 而操作系统写文件是以 4KB 作为单位的, 那么每写一个 InnoDB 的页到磁盘上, 操作系统需要写 4 个块。

而计算机硬件和操作系统, 在极端情况下(比如断电) 往往并不能保证这一操作的原子性, 16K 的数据, 写入 4K 时, 发生了系统断电或系统崩溃, 只有一部分写是成功的, 这种情况下会产生 partial page write(部分页写入) 问题。 这
时页数据出现不一样的情形, 从而形成一个"断裂"的页, 使数据产生混乱。 在InnoDB 存储引擎未使用 doublewrite 技术前, 曾经出现过因为部分写失效而导致数据丢失的情况。

doublewrite buffer 是 InnoDB 在表空间上的 128 个页(2 个区, extend1 和 extend2) , 大小是 2MB。 为了解决部分页写入问题, 当 MySQL 将脏数据 flush到数据文件的时候, 先使用 memcopy 将脏数据复制到内存中的一个区域(也是2M) , 之后通过这个内存区域再分 2 次, 每次写入 1MB 到系统表空间, 然后马上调用 fsync 函数, 同步到磁盘上。 在这个过程中是顺序写, 开销并不大, 在完成 doublewrite 写入后, 再将数据写入各数据文件文件, 这时是离散写入。

所以在正常的情况下, MySQL 写数据页时, 会写两遍到磁盘上, 第一遍是写到 doublewrite buffer, 第二遍是写到真正的数据文件中。 如果发生了极端情况(断电) , InnoDB 再次启动后, 发现了一个页数据已经损坏, 那么此时就可以从doublewrite buffer 中进行数据恢复了。

前面说过, 位于系统表空间上的 doublewrite buffer 实际上也是一个文件,写系统表空间会导致系统有更多的 fsync 操作, 而硬盘的 fsync 性能因素会降低MySQL 的整体性能。 不过在存储上, doublewrite 是在一个连续的存储空间, 所以硬盘在写数据的时候是顺序写, 而不是随机写, 这样性能影响不大, 相比不双写,降低了大概 5-10%左右。

所以, 在一些情况下可以关闭 doublewrite 以获取更高的性能。 比如在 slave上可以关闭, 因为即使出现了 partial page write 问题, 数据还是可以从中继日志中恢复。 比如某些文件系统 ZFS 本身有些文件系统本身就提供了部分写失效的防范机制, 也可以关闭。

在数据库异常关闭的情况下启动时, 都会做数据库恢复(redo) 操作, 恢复的过程中, 数据库都会检查页面是不是合法(校验等等) , 如果发现一个页面校验结果不一致, 则此时会用到双写这个功能。

有经验的同学也许会想到, 如果发生写失效, 可以通过重做日志(Redo Log)进行恢复啊! 但是要注意, 重做日志中记录的是对页的物理操作, 如偏移量 800,写’ aaaa’记录, 而不是页面的全量记录, 而如果发生 partial page write(部分页写入) 问题时, 出现问题的是未修改过的数据, 此时重做日志(Redo Log)无能为力。写 doublewrite buffer 成功了, 这个问题就不用担心了。

如果是写 doublewrite buffer 本身失败, 那么这些数据不会被写到磁盘,InnoDB 此时会从磁盘载入原始的数据, 然后通过 InnoDB 的事务日志来计算出正确的数据, 重新写入到 doublewrite buffer, 这个速度就比较慢了。如果 doublewrite
buffer 写成功的话,但是写数据文件失败, innodb 就不用通过事务日志来计算了,而是直接用 doublewrite buffer 的数据再写一遍, 速度上会快很多。

总体来说, doublewrite buffer 的作用有两个: 提高 innodb 把缓存的数据写到硬盘这个过程的安全性; 间接的好处就是, innodb 的事务日志不需要包含所有数据的前后映像,而是二进制变化量, 这可以节省大量的 IO

  • InnoDB 数据字典(Data Dictionary Header)

我们平时使用 INSERT 语句向表中插入的那些记录称之为用户数据, MySQL只是作为一个软件来为我们来保管这些数据, 提供方便的增删改查接口而已。 但是每当我们向一个表中插入一条记录的时候, MySQL 先要校验一下插入语句对应
的表存不存在, 插入的列和表中的列是否符合, 如果语法没有问题的话, 还需要知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面, 然后把记录插入对应索引的 B+树中。 所以说, MySQL 除了保存着我们插入的用户数据之外, 还需要保存许多额外的信息, 比方说:

某个表属于哪个表空间, 表里边有多少列, 表对应的每一个列的类型是什么,该表有多少索引, 每个索引对应哪几个字段, 该索引对应的根页面在哪个表空间的哪个页面, 该表有哪些外键, 外键对应哪个表的哪些列, 某个表空间对应文件
系统上文件路径是什么。

上述这些数据并不是我们使用 INSERT 语句插入的用户数据, 实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据, 这些数据也称为元数据。 InnoDB 存储引擎特意定义了一些列的内部系统表(internal system table)来记录这些这些元数据:

表名描述
SYS_TABLES整个 InnoDB 存储引擎中所有的表的信息
SYS_COLUMNS整个 InnoDB 存储引擎中所有的列的信息
SYS_INDEXES整个 InnoDB 存储引擎中所有的索引的信息
SYS_FIELDS整个 InnoDB 存储引擎中所有的索引对应的列的信息
SYS_FOREIGN整个 InnoDB 存储引擎中所有的外键的信息
SYS_FOREIGN_COLS整个 InnoDB 存储引擎中所有的外键对应列的信息
SYS_TABLESPACES整个 InnoDB 存储引擎中所有的表空间信息
SYS_DATAFILES整个 InnoDB 存储引擎中所有的表空间对应文件系统的文件路径信息
SYS_VIRTUAL整个 InnoDB 存储引擎中所有的虚拟生成列的信息

这些系统表也被称为数据字典, 它们都是以 B+树的形式保存在系统表空间的某些页面中, 其中 SYS_TABLES、 SYS_COLUMNS、 SYS_INDEXES、 SYS_FIELDS 这四个表尤其重要, 称之为基本系统表

这 4 个表是表中之表, 那这 4 个表的元数据去哪里获取呢? 只能把这 4 个表的元数据, 就是它们有哪些列、 哪些索引等信息硬编码到代码中, 然后 InnoDB又拿出一个固定的页面来记录这 4 个表的聚簇索引和二级索引对应的 B+树位置, 这个页面就是页号为 7 的页面 Data Dictionary Header, 类型为 SYS, 记录了数据字典的头部信息。 除了这 4 个表的 5 个索引的根页面信息外, 这个页号为 7的页面还记录了整个 InnoDB 存储引擎的一些全局属性, 比如 Row ID。

数据字典头部信息中有个 Max Row ID 字段, 我们说过如果我们不显式的为表定义主键, 而且表中也没有 UNIQUE 索引, 那么 InnoDB 存储引擎会默认为我们生成一个名为 row_id 的列作为主键。 因为它是主键, 所以每条记录的 row_id列的值不能重复。

原则上只要一个表中的 row_id 列不重复就可以了, 也就是说表 a 和表 b 拥有一样的 row_id 列也没啥关系, 不过 InnoDB 只提供了这个 Max Row ID 字段,不论哪个拥有 row_id 列的表插入一条记录时, 该记录的 row_id 列的值就是 Max Row ID 对应的值, 然后再把 Max Row ID 对应的值加 1, 也就是说这个 Max Row ID是全局共享的。

用户是不能直接访问 InnoDB 的这些内部系统表的, 除非你直接去解析系统表空间对应文件系统上的文件。 不过 InnoDB 考虑到查看这些表的内容可能有助于大家分析问题, 所以在系统数据库 information_schema 中提供了一些以innodb_sys 开头的表:
在这里插入图片描述
在 information_schema 数据库中的这些以 INNODB_SYS 开头的表并不是真正的内部系统表(内部系统表就是我们上边唠叨的以 SYS 开头的那些表) , 而是在存储引擎启动时读取这些以 SYS 开头的系统表, 然后填充到这些以INNODB_SYS开头的表中。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值