InnoDB 的存储方案

365 阅读15分钟

Mysql 是怎样运行的:从根上理解Mysql 读后总结

安装目录 与 数据目录

安装目录是用于存放 MySQL 服务及客户端程序的目录,bin 文件内存放各种 MySQL 启动脚本及工具

数据目录用于 MySQL 服务程序存放数据库文件的目录,所有的数据库数据都存放在该目录下

查看数据目录

通过执行 sql SHOW VARIABLES LIKE 'datadir'; 即可查看 MySQL 服务程序的数据目录是哪个

数据目录的结构

数据库在文件系统的表示

  1. 每个数据库都在数据目录下有一个同名的目录进行表示
  2. 这个数据库的目录下,都有一个 db.opt 的文件,用于存放这个数据库的各种属性,比如数据库的默认字符集和排序规则是什么

表在文件系统中的表示

表需要保存的主要是两方面的数据,一个是表的定义,包括这个表叫什么,有哪些字段,约束和索引等等,另外一个是插入到表中的数据

表的定义在 InnoDB 和 MyISAM 中,都会创建一个 表名.frm 的文件,用于专门描述表的结构,数据是以二进制的形式写入,所以直接打印会乱码

而对于数据保存的方式,不同的存储引擎则有不同的方法

InnoDB 保存数据

之前我们已经了解到,InnoDB 中,数据以 16kb 数据页的形式保存,而表就是以数据页为节点构成的一个树,不同页可以不连续的存在

为了更方便管理,InnoDB 以 表空间(table space) 或者 文件空间(table space),这个表空间是一个抽象概念,它对应文件系统中的一个或者多个真实文件,而一个表空间,又可以被划分为很多个页,用户数据就是存放在某个表空间的某个页里面,表空间主要有以下的几种类型:

  1. 系统表空间(system tablespace)

    系统表空间对应文件系统中的一个或多个文件,默认情况下 InnoDB 会在数据目录中,创建一个 ibdata1 的文件,这个就是系统表空间在文件系统中的表示,这个文件是会自动扩展文件大小的,空间不够用会自动扩展。MySQL 5.5.7 到 MySQL 5.6.6 之间的各个版本,我们表中的数据都会被默认存储到这个系统表空间

  2. 独立表空间(file-per-table tablespace)

    MySQL 5.6.6 之后的版本中,InnoDB 并不会都把数据存储到系统表空间,而是为每一个表都创建一个独立表空间,文件名就是表名,类似于:表名.ibd,这个文件我们就会用来存储数据和索引

  3. 其他表空间

    随着 MySQL 发展,引入了很多其他的表空间,这里先不做讨论

MyISAM 保存数据

MyISAM 中数据跟索引是分开保存的,所以在文件系统中也是通过不同的文件来保存索引跟数据,MyISAM 没有表空间的概念,表数据都存放在数据库对应的目录下,创建一个 表名.MYD(数据文件) 和一个 表名.MYI(索引文件)

视图在文件系统中的表示

视图是虚拟的表,也就是一个sql的别名,所以不需要保存数据仅需要存储虚拟表的结构即可,所以只会存储一个 视图.frm 的文件

文件系统对数据库的影响

因为数据库的数据实质上都是保存在文件系统之中,所以数据库就必然会受到文件系统的制约

  1. 数据库的名称及表名,不能超过文件系统所允许的最大长度

  2. 特殊字符的问题

    为避免数据库名和表名出现特殊字符导致文件系统不支持的问题,MySQL 会将数据库名及表名中除数字和拉丁字母以外的所有字符都在文件名中映射为 @+编码值 的形式,比如表名为 test? 的表名就会创建 test@003f.frm 的文件

  3. 文件长度受文件系统的最大长度限制

    对于 InnoDB 的独立表而言,每个表都会创建一个与表名相同的 .ibd 文件,对于 MyISAM 而言,数据和索引都会存放到 .MYD.MYI 文件,而这些文件的大小都受限于文件系统支持的最大文件大小

MySQL 系统数据库简介

MySQL 会创建几个的系统数据库

  1. mysql

    这个是数据库的核心,存储了MySQL 的用户信息和权限信息,一些存储过程和事件定义信息,一些帮助信息和时区信息

  2. information_schema

    这个数据库保存着 MySQL 维护所有其他数据库的信息,比如有哪些表,哪些视图和哪些触发器之类的,这些不是用户的真是数据,而是一些描述性的信息,有时候称之为元数据

  3. performance_schema

    这个数据库保存着 MySQL 服务器运行过程的状态信息,算算 MySQL 服务器的一个性能监控,包括统计最近执行了哪些语句,都花了多少时间和内存使用情况等等信息

  4. sys

    这个数据库主要通过视图的形式,把 information_schema 和 performance_schema 结合起来,更方便了解 MySQL 服务器的一些性能信息

InnoDB 的表空间

梳理下,InnoDB 的数据都是以 16KB 的页进行存储,而页格式中有页头部跟页尾两个通用的数据结构

  1. 页头部有页号(4字节),而页是保存至表空间中,所以单个表空间中,最多有 2^32 个页,也就是最多支持 64 TB的数据,页号顺序向下分配
  2. 某些类型的页可以组成链表,而链表的中的页不一定是真实物理上顺序存储的,而是通过页头部中的 FIL_PAGE_PREVFIL_PAGE_NEXT 指向本页的前驱和后继
  3. 页头部中还有 FIL_PAGE_TYPE 字段表示该页的类型,像数据页就是 0x45BF

独立表空间结构

区 (extent) 的概念

因为表空间中的页实在太多了,所以为了更方便管理, InnoDB 引入了区(extent) 的概念,对于 16KB 的页来说,连续 64 个页就是一个区,也就是说一个区默认会占用 1MB 的空间大小。不论是系统表空间还是独立表空间,都由若干个区组成,每 256 个区被划成一组

第一个组最开始的三个页面的类型是固定的,分别是:

  • FSP_HDR 类型 这个类型的页面,是用来登记整个表空间的一些属性和本组所有分区,每个表空间中仅有一个该类型的页面
  • IBUF_BITMAP类型 这个类型的页面是用来存储本组所有区的 INSERT BUFFER 的信息
  • INODE类型 这个类型的页面,存储了很多称为 INODE 的数据结构

其余组开始的两个页面的类型是固定的,分别是:

  • XDES类型 这个类型的页面主要用来登记本组 256 个区的属性
  • IBUF_BITMAP类型 这个类型的页面是用来存储本组所有区的 INSERT BUFFER 的信息
段 (segment) 的概念

按之前的理解,数据通过页组成双向链表,而索引则以页为节点,通过访问索引能快速定位数据所在页的边界,然后通过顺序遍历查找出所有的数据记录来。这里有个问题是,如果索引的每个数据页在表空间中距离都非常远,那么索引的效率依然非常低,因为每次数据页的访问都是随机IO,假如数据是放置在相邻的几个页中,就能快速的读取,这就是顺序IO顺序IO 往往比 随机IO 快几个数量级

这里可以看出,数据页的距离,对性能有非常大的影响,所以为了提高效率,引入了区的概念,去就是物理上连续的 64 个页,在数据量大的时候,就不再通过页来申请,而是直接以区为单位进行申请,这样可能会出现区内数据没填满导致空间的浪费,但是提高了性能

并且在对 b+ 树进行扫描的时候,一般是先对非叶子节点进行访问确定数据记录在哪些叶子节点中,再读取叶子节点的数据,进行遍历访问,所以 InnoDB 中非叶子节点有自己的区,而叶子节点也有自己的区,这里存放非叶子节点的区的集合就是一个 段 (segment) ,而存放叶子节点的区的集合也是一个段

对于数据记录非常少的表,数据并不会直接就以区进行申请,而是在 碎片区 (fragment) 中进行分配,碎片区就是多个表共享的一个分配空间的区,直接归属于表空间并不属于任何一个段

所以某个段分配存储空间的策略类似于:

  1. 现在碎片区中分配空间用于表插入数据
  2. 某个段已经占用了32个碎片区页面之后,就会以区为单位来分配空间

所以段更准确的说,应该是某些零散的页面以及一些完整的区的集合

区的类型

根据之前对段的了解,我们将区分为好几种 FREE(空闲的区)FREE_FREG(有剩余空间的碎片区)FULL(没有生育空间的碎片区)FSEG(附属某个段的区)

为了方便管理区,InnoDB 设计了一个 XDES Entry 的数据结构,每个区都有一个这个的数据结构,用于记录该区的一些属性,结构如下:

名称 大小
Segment ID 8字节
List Node (Prev Node Page Number) 4字节
List Node (Prev Node Offset) 2字节
List Node (Next Node Page Number) 4字节
List Node (Next Node Offset) 2字节
Stage 4字节
Page Stage Bitmap 16字节

**Segment ID **:

每一个段都有一个编号,用 ID 表示,Segment ID 就是该区所在的段,前提是该区归属于某个段,不然该字段没意义

List Node:

12字节,该部分可以将若干个 XDES Entry 结构串联成一个链表,Prev Node Page NumberPrev Node Offset 就是指向上一个 XDES Entry 的指针,而 Next Node Page NumberNext Node Offset 就是指向下一个 XDES Entry 的指针

State:

这个字段表示区的类型,可选值就是区的四种类型

Page Stage Bitmap:

这部分有16字节,128位,每2位表示一个页,第一个位表示该页是否空闲,第二个位暂时未使用

XDES Entry 链表

这时候插入一条记录时:

  • 如果段中数据还比较少,首先会找到有剩余空间的碎片区,找到了就在这个碎片区中分配页用于插入数据,未找到就在空闲区中分配一个区标记为碎片区,然后分配页用于插入数据

    这时有个问题就是如何快速找到空闲的碎片区以及空闲区,当数据库数据已经非常多的时候,遍历所有区的效率就会非常的低,所以 InnoDB 将所有空闲区通过XDES Entry 串联成一个链表,称之为 FREE 链表,与之类似的还有 FREE_FRAG链表以及 FULL_FRAG链表

    现在的流程就是首先在FREE_FRAG链表 中的头节点中的一些页用于数据存储,如果所有的页都用完了,就将该节点从 FREE_FRAG链表 移至 FULL_FRAG链表 中,如果FREE_FRAG链表 一个节点也没有,就从 FREE链表 中取一个节点加入 FREE_FRAG链表

  • 如果段中以及占满了 32 个碎片页之后,这个时候就会申请一个完整的区进行存储

    这里有个问题就是,如何快速找到某个段对应的区呢,跟碎片区的链表相似,InnoDB 为每个段都创建了三个链表 FREE链表NOT_FULL链表 以及 FULL链表

    FREE链表:区别于表空间的FREE链表,这里记录的都是由各个碎片区分配的页面,当段中占满了32个碎片页后,就是通过这个链表快速定位数据所在页

    NOT_FULL链表 :同一个段中,仍然有空闲空间的区都会加入到这个链表中

    FULL链表:同一个段中,以及没有空闲空间的区都会加入到这个链表中

链表基节点

上面介绍了,表空间有自己的链表,段中也分配了三个链表,那如果找到这些链表呢,InnoDB 设计了链表的基节点(List Base Node)

名称 描述 大小
List Length 链表一共有多少节点 4字节
First Node Page Number 链表头节点空间位置 4字节
First Node Offset 链表头节点空间位置 2字节
Last Node Page Number 链表尾节点空间位置 4字节
Last Node Offset 链表尾节点空间位置 2字节

每个链表都有一个这样的数据结构,而链表基节点数量是固定的,存储在表空间的固定位置,所以就能很方便的找到这些链表

段的结构

不同于区,段并不是连续的物理空间,而是一个逻辑概念,由若干个零散的页面和完整的区组成,InnoDB 为每个段都定义了一个 INODE ENTRY 的数据结构来存储段中的属性

名称 描述 大小
Segment ID 段的唯一标识 8字节
NOT_FULL_N_USER NOT_FULL链表使用了多少个页面,下次分配空间时直接根据这个值进行定位而不是遍历 4字节
List Base Node For FREE List FREE链表基节点 16字节
List Base Node For NOT_FULL List NOT_FULL链表基节点 16字节
List Base Node For FULL List FULL链表基节点 16字节
Magic Number 标记 INODE ENTRY 是否被初始化 4字节
Fragment Array Entry 0 碎片区页面页号 4字节
Fragment Array Entry 1 碎片区页面页号 4字节
... ... 碎片区页面页号 4字节
Fragment Array Entry 31 碎片区页面页号 4字节
FSP_HDR 类型

第一个组的第一个固定的页面,它主要用于存储表空间的整体属性及第一个组内256个区对应的 XDES Entry

这里表空间的整体属性就包括由表空间负责管理的 FREE 链表FREE_FRAG链表 以及 FULL_FRAG链表 的基节点,还有这个表空间所有段的 INODE ENTRY 的基节点

另外每一页能存放的数据有限,所以将区按每 256 为一组,FSP_HDR页面 就存放了第一组的所有区的 XDES Entry ,而每个区对应的 XDES Entry 的位置是固定的,所以访问也非常的容易

XDES 类型

除第一组以外,其余组的第一个页面固定是 XDES页面,与 FSP_HDR页面 的区别在于,XDES页面 没有表属性相关的内容,仅仅是存放本组的所有区对应的 XDES Entry

INODE 类型

第一个分组的第三个页面固定是 INODE 页面,对于每个索引, InnoDB 都会创建两个段,而为了方便管理这些段又设计了 INODE ENTRY,这些数据都是存放在 INODE 页面

INODE 页面 可以保存一定数量的段,当段的数据超过85,则需要创建新的 INODE 页面,而 INODE 页面 之间则是通过 List Node for INODE Page List 这个属性标记前后页面

另外该页面会存储 INODE ENTRY 信息

系统表空间

系统表结构与独立表结构类似,比较大的区别在于,整个 MySQL 服务只有一个系统表空间,所以表空间的开头,有很多记录系统属性的页面

InnoDB 数据字典

当我们通过 SQL 要操作数据时,比如插入一条记录,MySQL 服务需要验证表是否存在,列是否符合,语法校验完还需要知道聚簇索引和所有的二级索引对应的跟页面在哪个表空间的哪个页面,所以 MySQL 会保存大量的额外信息,这些为了更好管理用户数据的额外数据,就是 元数据,InnoDB 定义了一些内部系统表用于记录这些 元数据

表名 描述
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 存储引擎中所有的虚拟生成列的信息

其中 SYS_TABLES,SYS_COLUMNS,SYS_INDEXES 及 SYS_FIELDS 四个表最重要,包含了其他所有系统表及用户自定义表的元数据,而这几个表的元数据则是硬编码到服务程序中,通过 SYS 页面 专门用于存储

information_schema 系统数据库

用户不能直接访问 InnoDB 的内部系统表,所以为了方便分析问题,在系统数据库 information_schema 中提供一些以 innodb_sys 开头的表,这些表并不是真正的表,而是系统读取了内部系统表后将数据填充到这些表中供用户查看