数据库系列之InnoDB磁盘文件结构

InnoDB存储引擎包括内存部分结构和磁盘中的结构,在上一章节中介绍了内存部分,这里介绍磁盘中的文件,包括表空间文件、redo和undo log,并结合工具分析数据页的结构。


1、InnoDB磁盘上的结构

在这里插入图片描述

InnoDB磁盘中的结构分为几类类:表空间、Redo Log、Doublewrite Buffer、undo log

  • 表空间分为系统表空间(ibdata1文件)、临时表空间、常规表空间、Undo表空间以及file-per-table表空间。系统表空间又包括双写缓冲区(Doublewrite Buffer)、Change Buffer等
  • Redo log:存储的是log buffer刷到磁盘的数据
  • Doublewrite buffer:innodb 将数据页写到文件之前存放的位置。8.0.20版本之前,doublewrite buffer存放在InnoDB系统表空间中,8.0.20版本后存放在doublewrite中
  • Undo log:存在于global临时表空间中,用于事务的回滚
1.1 InnoDB表空间概述

InnoDB表空间类型包括系统表空间、File-Per-Table表空间,常规表空间,Undo表空间,临时表空间等。

  • 系统表空间:数据存放在名为“ibdata1”的文件中,包含change buffer、双写缓冲以及undo日志,以及在系统表空间创建的表的数据和索引。在8.0版本之前系统表空间中包含data dictionary,8.0版本后元数据保存在data dictionary中。
  • 常规表空间:类似系统表空间,也是一种共享的表空间,可以通过 CREATE TABLESPACE 创建常规表空间,多个表可共享一个常规表空间,各数据表的数据分散存储后缀为“.idb”的不同文件中。
  • File-Per-Table表空间:MySQL InnoDB新版本提供了 innodb_file_per_table 选项,每个表可以有单独的表空间数据文件(.ibd),而不是全部放到系统表空间数据文件 ibdata1 中。在 MySQL8.0中该选项默认开启。
  • Undo表空间: Undo表空间存储的是Undo日志,在MySQL初始化的时候会创建两个默认的undo表空间,通过innodb_undo_directory参数指定存放的位置,默认文件名为undo_001和undo_002。如果未指定,默认会存放在data dictionary中,默认文件名为innodb_undo_001和innodb_undo_002。
  • 临时表空间是非压缩的临时表的存储空间,分为session临时表空间和全局临时表空间。Session临时表空间在session请求时候创建的,最大分配2个,一个是用户创建的临时表空间,一个是优化器创建的临时表空间。全局临时表空间默认是数据目录的ibtmp1文件,所有临时表共享。
1.1.1 表空间文件结构

在这里插入图片描述
InnoDB表空间文件结构上分为段Segment、区Extend和页Page:

1)段(Segment)

段是磁盘上空间分配和回收的申请者,是一个逻辑概念,用来管理物理文件。常见的段有数据段、索引段、回滚段等。段是为了保持叶子节点在磁盘上的连续,可以实现更好的顺序I/O操作,因为这些叶子节点包含实际的表数据。其中索引段就是非叶子结点部分,而数据段就是叶子结点部分,回滚段用于数据的回滚和多版本控制。一个段包含256个区(256M大小)。

2)区(Extend)

区是由连续页组成的空间,每个区的默认大小都是1MB,一个区中有64个连续的页。为了保证区中页的连续性,扩展的时候InnoDB存储引擎一次从磁盘申请4~5个区。

3)页(Page)

Page是整个InnoDB存储的最基本构件,也是InnoDB磁盘管理的最小单位,与数据库相关的所有内容都存储在这种Page结构里。所有页的结构都是一样的,分为文件头(前38字节),页数据和文件尾(后8字节)。常见的有FSP_HDR,INODE,INDEX 等类型。
在这里插入图片描述

  • FILE_SPACE_HEADER 页:用于存储区的元信息。ibd文件的第一页FSP_HDR页通常就用于存储区的元信息,里面的256个XDES(extent descriptors)项存储了256个区的元信息,包括区的使用情况和区里面页的使用情况。
  • IBUF_BITMAP页:用于记录change buffer的使用情况。
  • INODE页:用于记录文件段(FSEG)的信息,每页有85个INODE entry,每个INODE entry占用192字节,用于描述一个文件段。每个INODE entry包括文件段ID、属于该段的区的信息以及碎片页数组。区信息包括 FREE(完全空闲的区), NOT_FULL(至少使用了一个页的区), FULL(没空闲页的区)三种类型的区的List Base Node(包含链表长度和头尾页号和偏移的结构体)。碎片页数组则是不同于分配整个区的单独分配的32个页。
  • INDEX页:索引页的叶子结点的data就是数据,如聚集索引存储的行数据,辅助索引存储的主键值。

4)行(Row)

InnoDB存储引擎时面向列的,也就是数据是按照行存储的

1.1.2 Page结构

InnoDB数据库页由7部分组成:File header(文件头)、Page Header(页头)、Infimum和Supremum Records、User Records(行记录)、Free Space(空闲空间)、Page Directory(页目录)和File Tailer(文件结尾信息)。其中File header、Page Header和File Tailer大型是固定的,分别为36、56和8字节,这些空间用来标记该页的一些信息。

1)File header

Page中的的File Header保存了两个指针,分别指向前一个Page和后一个Page,头部还有Page的类型信息和用来唯一标识Page的编号。根据这两个指针,可以想象出Page链接起来就是一个双向链表的结构。File header字段如下:
在这里插入图片描述
InnoDB中存储页的类型如下图所示:
在这里插入图片描述
2)Page Header

Page header用来记录数据页的状态信息,由14个部分组成,共占用56个字节。
在这里插入图片描述
3)Infimum和Supremum Records

InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录比该页中任何主键值都要小的值,Supremum是比任何可能大的值还大。这两个值在页创建时候创建,并且不会被删除。

4)User Record和Free Space

User Record是实际存储行记录的内容,free space是空闲空间,在一条记录被删除后,该空间会加入到空闲链表中。User Record在Page内以单链表的形式存在,随着新数据的插入和旧数据的删除,数据物理顺序会变得混乱,但他们依然保持着逻辑上的先后顺序(Next指针)。通过记录头中的next record可以串联得到一个可用空间链表。把User Record的组织形式和若干Page组合起来,就看到了稍微完整的形式。
在这里插入图片描述
5)File Trailer

为了检测页是否已完整的写入磁盘,InnoDB存储引擎的页中设置了File Trailer部分。File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节,前4字节代表该页的checksum值,最后4字节和File Header中的FIL_PAGE_LSN相同。将这两个值和File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值进行比较,看是否一致来保证页的完整性。

6)整个page结构图如下
在这里插入图片描述

1.1.3 表文件ibd结构

InnoDB表空间文件.ibd初始大小为96K,而InnoDB默认页大小为16K,页大小也可以通过innodb_page_size配置为4K, 8K…64K等。在ibd文件中,0-16KB偏移量即为0号数据页,16KB-32KB的为1号数据页,以此类推。页的头尾除了一些元信息外,还有Checksum校验值,这些校验值在写入磁盘前计算得到,当从磁盘中读取时,重新计算校验值并与数据页中存储的对比,如果发现不同,则会导致 MySQL 崩溃。ibd文件存储结构如下所示:
在这里插入图片描述

InnoDB页分为INDEX页、Undo页、系统页、IBUF_BITMAP页、INODE页等多种。

  • 第0页是FSP_HDR页,主要用于跟踪表空间、空闲链表、碎片页以及区等信息。
  • 第1页是IBUF_BITMAP页,保存Change Buffer的位图。
  • 第2页是INODE页,用于存储区和单独分配的碎片页信息,包括FULL、FREE、NOT_FULL 等页列表的基础结点信息(基础结点信息记录了列表的起始和结束页号和偏移等),这些结点指向的是FSP_HDR页中的项,用于记录页的使用情况。
  • 第3页开始是索引页INDEX(B-tree node),从0xc000(每页16K)

开始,后面还有些分配的未使用的页。
使用ruby脚本innodb_space可以查看表的page信息:

[root@tango-centos01 mysql]# innodb_space -s ibdata1 -T tango/tb01  space-page-type-regions
start       end         count       type                
0           0           1           FSP_HDR             
1           1           1           IBUF_BITMAP         
2           2           1           INODE               
3           3           1           INDEX               
4           4           1           FREE (INDEX)        
5           5           1           FREE (ALLOCATED)    

可以在innodb_sys_tables表中查到表tango.tb01的表空间ID为60,然后可以在 innodb_buffer_page查到所有页信息,一共4个页,分别是FSP_HDR、IBUF_BITMAP、INODE和INDEX。

mysql> select * from information_schema.innodb_sys_tables where name='tango/tb01';
+----------+------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME       | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+------------+------+--------+-------+-------------+------------+---------------+------------+
|       49 | tango/tb01 |   33 |      5 |    60 | Barracuda   | Dynamic    |             0 | Single     |
+----------+------------+------+--------+-------+-------------+------------+---------------+------------+
1 row in set (0.00 sec)

mysql> select space,page_number,page_type,access_time,table_name  from information_schema.innodb_buffer_page where space=60;
+-------+-------------+-------------------+-------------+----------------+
| space | page_number | page_type         | access_time | table_name     |
+-------+-------------+-------------------+-------------+----------------+
|    60 |           0 | FILE_SPACE_HEADER |      672805 | NULL           |
|    60 |           1 | IBUF_BITMAP       |      296460 | NULL           |
|    60 |           2 | INODE             |      672805 | NULL           |
|    60 |           3 | INDEX             |      662508 | `tango`.`tb01` |
|    60 |           4 | INDEX             |           0 | `tango`.`tb01` |
+-------+-------------+-------------------+-------------+----------------+
5 rows in set (0.04 sec)
1.1.4 索引页结构

InnoDB引擎索引页的结构如下图,可以用hexdump查看tb01.ibd文件,然后对照InnoDB页的结构分析下各个页的字段。
在这里插入图片描述

[root@tango-centos01 tango]# hexdump -C tb01.ibd
0000c000  d8 72 c4 db 00 00 00 03  ff ff ff ff ff ff ff ff  |.r..............|
0000c010  00 00 00 00 00 2c fc 74  45 bf 00 00 00 00 00 00  |.....,.tE.......|
0000c020  00 00 00 00 00 3c 00 02  01 66 80 09 00 00 00 00  |.....<...f......|
0000c030  01 4b 00 02 00 02 00 07  00 00 00 00 00 00 00 00  |.K..............|
0000c040  00 00 00 00 00 00 00 00  00 32 00 00 00 3c 00 00  |.........2...<..|
0000c050  00 02 00 f2 00 00 00 3c  00 00 00 02 00 32 01 00  |.......<.....2..|
0000c060  02 00 1c 69 6e 66 69 6d  75 6d 00 08 00 0b 00 00  |...infimum......|
0000c070  73 75 70 72 65 6d 75 6d  0a 00 00 00 10 00 44 80  |supremum......D.|
0000c080  00 00 01 00 00 00 00 2d  08 a8 00 00 01 1c 01 10  |.......-........|
0000c090  62 6a 20 20 20 20 20 20  20 20 0a 00 00 00 18 00  |bj        ......|
0000c0a0  88 80 00 00 06 00 00 00  00 41 52 ae 00 00 01 22  |.........AR...."|
0000c0b0  01 10 61 68 20 20 20 20  20 20 20 20 0a 00 00 00  |..ah        ....|
0000c0c0  20 00 22 80 00 00 03 00  00 00 00 2d 0e ac 00 00  | ."........-....|
0000c0d0  01 20 01 10 67 7a 20 20  20 20 20 20 20 20 0a 00  |. ..gz        ..|
0000c0e0  00 00 28 00 22 80 00 00  04 00 00 00 00 2d 0f ad  |..(."........-..|
0000c0f0  00 00 01 21 01 10 73 68  20 20 20 20 20 20 20 20  |...!..sh        |
0000c100  0a 00 00 00 30 ff 9a 80  00 00 05 00 00 00 00 3d  |....0..........=|
0000c110  52 36 00 00 01 4c 06 59  6e 68 20 20 20 20 20 20  |R6...L.Ynh      |
0000c120  20 20 0a 00 00 00 38 00  22 80 00 00 0a 00 00 00  |  ....8.".......|
0000c130  00 43 89 cf 00 00 01 60  01 10 61 61 20 20 20 20  |.C.....`..aa    |
0000c140  20 20 20 20 0a 00 00 00  40 ff 25 80 00 00 0c 00  |    ....@.%.....|
0000c150  00 00 00 43 8b d0 00 00  01 62 01 10 62 62 20 20  |...C.....b..bb  |
0000c160  20 20 20 20 20 20 00 00  00 00 00 00 00 00 00 00  |      ..........|
0000c170  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
  • FIL Header(38字节): 记录文件头信息。前4字节d8 72 c4 db是 checksum,接着00 00 00 03是页偏移值 3,即这是第3页。接着4字节是上一页偏移值,因为只有一个数据页,所以这里为ff ff ff ff,接着4字节是下一页偏移值ff ff ff ff。然后8字节00 00 00 00 00 2c fc 74是日志序列号 LSN。随后的2字节45 bf是页类型,代表是INDEX页。接着8 字节00 00 00 00 00 00 00 00表示被更新到的LSN,在File-Per-Table表空间中都是0。然后 4 字节00 00 00 3c表示该数据页属于的表tb01的表空间ID是 0x3c(60)。
  • INDEX Header(36字节): 记录的是INDEX页的状态信息。前2字节00 02表示页目录的slot数目为2;接着2字节01 66是页中第一个记录的指针。80 09是这页的格式为DYNAMIC和记录数9(包括2条System Records和插入的7条记录)。接着00 00是可重用空间首指针,再后面2字节00 00是已删除记录数;01 4b是最后插入记录的位置偏移,即最后插入位置是 0xc014b,即第2条记录开始地址。00 02是最后插入的方向,2 表示PAGE_DIRECTION_RIGHT,即自增长方式插入。00 02指一个方向连续插入的数量,这里为2。接着的00 07是INDEX页中的真实记录数,有7条记录。然后8字节00…00为修改该页的最大事务ID,这个值只在辅助索引中存在,这里为0。接着2字节00 00为页在索引树的层级,0表示叶子结点。最后8个字节 00…32为索引ID 50(索引ID可以在information_schema.INNODB_SYS_INDEXES 中查询,可以确认50正好是表tb01的主索引)。
mysql> select * from information_schema.INNODB_SYS_INDEXES where space=60;
+----------+---------+----------+------+----------+---------+-------+-----------------+
| INDEX_ID | NAME    | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
+----------+---------+----------+------+----------+---------+-------+-----------------+
|       50 | PRIMARY |       49 |    3 |        1 |       3 |    60 |              50 |
+----------+---------+----------+------+----------+---------+-------+-----------------+
1 row in set (0.00 sec)
  • FSEG Header:这是INDEX页中的根结点才有的,非根结点的为0。前10字节 00 00 00 3c 00 00 00 02 00 f2是叶子结点所在段的segment header,分别记录了叶子结点的表空间ID 0x3c,INODE页的页号2和INODE项偏移0xf2。而后10字节00 00 00 3c 00 00 00 02 00 32是非叶子结点所在段的segment header,偏移分别是0x02和0x32,即INODE页的前2个Entry,文件段ID分别是1和2。FSEG Header中存储了该INDEX页的INODE项,INODE项里面则记录了该页存储所在的文件段以及文件段页的使用情况。对于File-Per-Table情况下,每个单独的表空间文件的FSP_HDR页负责管理页使用情况。
  • System Records(26字节): 每个INDEX页都有两条虚拟记录infimum和supremum,用于限定记录的边界,各占13个字节。其中记录头的5个字节分别标识了拥有记录的数目和类型(拥有记录数目是即后面页目录部分的owned值,当前页目录只有两个槽,infimum拥有记录数只有它自己为1,而supremum拥有我们插入的2条记录和它自己,故为3)、下一条记录的偏移0x1c,即位置是 0xc07f,这就是我们实际记录开始位置。后面8个字节为 infimum + 空值,supremum类似,只是它下一条记录偏移为0。
0000c060  02 00 1c 69 6e 66 69 6d  75 6d 00 08 00 0b 00 00  |...infimum......|
0000c070  73 75 70 72 65 6d 75 6d  0a 00 00 00 10 00 44 80  |supremum......D.|
  • User Records: 接下来是7条我们插入的记录。
  • Page Directory(4字节):因为页目录的slot只有2个,每个slot占2字节,故页目录为 00 70 00 63这4字节,存储的是相对于最初行的位置。其中0xc063正好是 infimum 记录的开始位置,而0xc070正好是 supremum 记录的开始位置。使用页目录进行二分查找,可以加速查询。
  • FIL Tail (8字节): 最后8字节为d8 72 c4 db 00 2c fc 74,其中d8 72 c4 db为 checknum,跟FIL Header的checksum一样。后4字节00 2c fc 74与FIL Header的LSN的后4个字节一致。

也可以通过 innodb_ruby 工具来分析表空间文件。

[root@tango-centos01 mysql]# innodb_space -s ibdata1 -T tango/tb01 space-indexes
id          name                            root        fseg        fseg_id     used        allocated   fill_factor 
50          PRIMARY                         3           internal    1           1           1           100.00%     
50          PRIMARY                         3           leaf        2           0           0           0.00%

[root@tango-centos01 mysql]# innodb_space -s ibdata1 -T tango/tb01 -p 3 page-records
Record 127: (id=1) → (name="bj")
Record 195: (id=3) → (name="gz")
Record 229: (id=4) → (name="sh")
Record 263: (id=5) → (name="nh")
Record 161: (id=6) → (name="ah")
Record 297: (id=10) → (name="aa")
Record 331: (id=12) → (name="bb")

上面的结果是解析聚集索引根节点页的信息,1行就代表使用了1个page,所以,叶子节点共使用了7个page,根节点使用了1个page

1.1.5 Undo日志

MySQL的MVCC(多版本并发控制)依赖Undo Log实现。MySQL的表空间文件tb01.ibd存储的是记录最新值,每个记录都有一个回滚指针,指向该记录的最近一条Undo记录,而每条Undo记录都会指向它的前一条Undo记录,如下图所示。默认情况下undo log存储在系统表空间 ibdata1 中。

mysql> insert into tango.tb01 values(100,'xxx');
Query OK, 1 row affected (0.12 sec)

插入一条数据后,可以发现当前tb01.ibd文件中的记录是(100,‘xxx’),而 Undo Log此时有一条insert的记录。如下:

[root@tango-centos01 mysql]# innodb_space -s ibdata1 -T tango/tb01 -p 3 -R 365 record-history
Transaction   Type                Undo record
(n/a)         insert              (id=100) → ()

执行后面的update语句,可以看到undo log如下:

[root@tango-centos01 mysql]# innodb_space -s ibdata1 -T tango/tb01 -p 3 -R 365 record-history
Transaction   Type                Undo record
71952         update_existing     (id=100) → (name="xxx")
(n/a)         insert              (id=100) → ()

需要注意的是,Undo Log在事务执行过程中就会产生,事务提交后才会持久化,如果事务回滚了则Undo Log也会删除。

1.1.6 Doublewrite Buffer双写缓冲

InnoDB的记录更新流程是先在Buffer Pool中更新,并将更新记录到Redo Log文件中,Buffer Pool中的记录会标记为脏数据并定期刷到磁盘。由于InnoDB默认Page大小是16KB,而磁盘通常以扇区为单位写入,每次默认只能写入512个字节,无法保证16K数据可以原子的写入。如果写入过程发生故障(比如机器掉电或者操作系统崩溃),会出现页的部分写入(partial page writes),导致难以恢复。因为MySQL的重做日志采用的是物理逻辑日志,即页间是物理信息,而页内是逻辑信息,在发生页部分写入时,无法确认数据页的具体修改而导致难以恢复。

MySQL的数据页在真正写入到表空间文件前,会先写到系统表空间文件的一段连续区域双写缓冲(Double-Write Buffer,默认大小为 2MB,128个页)并fsync落盘,等双写缓冲写入成功后才会将数据页写到实际表空间的位置。因为双写缓冲和数据页的写入时机不一致,如果在写入双写缓冲出错,可以直接丢弃该缓冲页,而如果是写入数据页时出错,则可以根据双写缓冲区数据恢复表空间文件。

1.2 Redo Log

引入buffer pool会导致更新的数据不会实时持久化到磁盘,当系统崩溃时,虽然buffer pool中的数据丢失,数据没有持久化,但是系统可以根据Redo Log的内容,将所有数据恢复到最新的状态。redo log在磁盘上作为一个独立的文件存在。默认情况下会有两个文件,名称分别为 ib_logfile0和ib_logfile1。参数innodb_log_file_size指定了redo log的大小;innodb_log_file_in_group指定了redo log的数量,默认为2。

Innodb在日志系统里面定义了log block的概念,其实log block就是一个512字节的数据块,这个数据块包括块头、日志信息和块的checksum.其结构如下:
在这里插入图片描述

如果每个页中产生的重做日志数量大于512字节,那么需要分割多个重做日志块进行存储,此外,由于重做日志快的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,重做日志快除了日志本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用4字节。故每个重做日志块实际可以存储的大小为496字节(512-12-4)。

到这里,InnoDB存储引擎有关内存和磁盘上的结构已经介绍完毕。内存部分参考“数据库系列之InnoDB存储引擎解密”。


参考资料

  1. 《MySQL技术内幕:InnoDB存储引擎》第2版,姜承尧著
  2. https://blog.csdn.net/weixin_43927408/article/details/95228030
  3. https://www.jianshu.com/p/d4cc0ea9d097
  4. https://blog.csdn.net/bohu83/article/details/81481184
  5. https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html

转载请注明原文地址:https://blog.csdn.net/solihawk/article/details/120104404
文章会同步在公众号“牧羊人的方向”更新,感兴趣的可以关注公众号,谢谢!
在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值