相信通过前面几个小节的介绍,读者已经知道页是 InnoDB存储引擎管理数据库的最小磁盘单位。页类型为B-Tree Node的页存放的即是表中行的实际数据了。在这一节中,我们将从底层具体地介绍 InnoDB数据页的内部存储结构注意 InnoDB公司本身并没有详细介绍其页结构的实现, MySQL的官方手册中也基本没有提及 InnoDB存储引擎页的内部结构。
InnoDB数据页由以下7个部分组成,如图所示。
其中 File header、 Page Header、 File trailer的大小是固定的,分别为38、56、8字节,这些空间用来标记该页的一些信息,如 Checksum,数据页所在B+树索引的层数等。 User Records、 Free Space、 Page Directory这些部分为实际的行记录存储空间,因此大小是动态的。
File header用来记录页的一些头信息,由表4-3中8个部分组成,共占用38字节。
名称 | 大小(字节) | 说明 |
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 当 MySQL为 MySQL40.14之前的版本时,该值为0。在之后的 MySQL版本中,该值代表页的 checksum值(一种新的 checksum值) |
FIL_PAGE_OFFSET | 4 | 表空间中页的偏移值。如某独立表空间a.ibd的大小为1GB,如果页的FIL_PAGE_OFFSET大小为16KB,那么总共有65536个页。 FIL_PAGE_OFFSET表示该页在所有页中的位置。若此表空间的ID为10,那么搜索页(10,1)就表示查找表a中的第二个页 |
FIL_PAGE_PREV | 4 | 当前页的上一个页,B+Tree特性决定了叶子节点必须是双向列表 |
FIL_PAGE_NEXT | 4 | 当前页的下一个页,B+Tree特性决定了叶子节点必须是双向列表 |
FIL_PAGE_LSN | 8 | 该值代表该页最后被修改的日志序列位置LSN( Log Sequence Number) |
FIL_PAGE_TYPE | 2 | InnoDB存储引擎页的类型。常见的类型见下表。记住0x45BF,该值代表了存放的是数据页,即实际行记录的存储空间 |
LSN_FLUSH_LSN | 8 | 该值仅在系统表空间的一个页中定义,代表文件至少被更新到了该LSN_FLUSH_LSN值。对于独立表空间,该值都为0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 从 MySQL4.1开始,该值代表页属于哪个表空间 |
名称 | 十六进制 | 解释 |
FIL_PAGE_INDEX | 0x45BF | B+树叶节点 |
FIL_PAGE_UNDO_LOG | 0x0002 | Undo Log页 |
FIL_PAGE_INODE | 0x0003 | 索引节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 该页为最新分配 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | File Space Header |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
接着 File header部分的是 Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节,如下表所示。
名称 | 大小(字节) | 说明 |
PAGE_N_DIR SLOTS | 2 | 在 Page Directory(页目录)中的Slot(槽)数,“Page Directory”小节中会介绍 |
PAGE_HEAP_TOP | 2 | 堆中第一个记录的指针,记录在页中是根据堆的形式存放的 |
PAGE_N_HEAP | 2 | 堆中的记录数。一共占用2字节,但是第15位表示行记录格式 |
PAGE_FREE | 2 | 指向可重用空间的首指针 |
PAGE_GARBAGE | 2 | 已删除记录的字节数,即行记录结构中 delete flag为1的记录大小的总数 |
PAGE_LAST_INSERT | 2 | 最后插入记录的位置 |
PAGE_DIRECTION | 2 | 最后插入的方向。可能的取值为: PAGE_LEFT(0x01) PAGE_RIGHT(0x02) PAGE_DIRECTION PAGE_SAME_REO(0x03) PAGE_SAME_PAGE(0x04) PAGE_NO_DIRECTION (Ox05) |
PAGE_N_DIRECTION | 2 | 一个方向连续插人记录的数量 |
PAGE_NRECS | 2 | 该页中记录的数量 |
PAGE_MAX_TRX_ID | 8 | 修改当前页的最大事务ID,注意该值仅在 Secondary Index中定义 |
PAGE_LEVEL | 2 | 当前页在索引树中的位置,0x00代表叶节点,即叶节点总是在第0层 |
PAGE_INDEX_ID | 8 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10 | B+树数据页非叶节点所在段的 segment header。注意该值仅在B+树的Root页中定义 |
PAGE_BTR_SEG_TOP | 10 | B+树数据页所在段的 segment header。注意该值仅在B+树的Root页中定义 |
在 InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。 Infimum记录是比该页中任何主键值都要小的值, Supremum指比任何可能大的值还要大的值。这两个值在页创建时被建立,并且在任何情况下不会被删除。在 Compact行格式和 Redundant行格式下,两者占用的字节数各不相同。下图显示了 Infimum和Supremum记录。
User Record就是之前讨论过的部分,即实际存储行记录的内容。再次强调, InnoDB存储引擎表总是B+树索引组织的。
Free Space很明显指的就是空闲空间,同样也是个链表数据结构。在一条记录被删除后,该空间会被加入到空闲链表中。
Page Directory(页目录)中存放了记录的相对位置(注意,这里存放的是页相对位置,而不是偏移量),有些时候这些记录指针称为Slots(槽)或目录槽( Directory Slots)。与其他数据库系统不同的是,在 InnoDB中并不是每个记录拥有一个槽, InnoDB存储引擎的槽是一个稀疏目录( sparse directory),即一个槽中可能包含多个记录。伪记录 Infimum的 n_owned值总是为1,记录 Supremum的n_owned的取值范围为[1,8],其他用户记录 n_owned的取值范围为[4,8]。当记录被插入或删除时需要对槽进行分裂或平衡的维护操作。
在 Slots中记录按照索引键值顺序存放,这样可以利用二叉查找迅速找到记录的指针。假设有(i,'d,'c',"b','e',"g"',"l',"h','f",∵j',"k',"a')同时假设一个槽中包含4条记录,则 Slots中的记录可能是('a','e','i')。
由于在 InnoDB存储引擎中 Page Direcotry是稀疏目录,二叉查找的结果只是一个粗略的结果,因此 InnoDB存储引擎必须通过recorder header中的 next record来继续查找相关记录。同时, Page Directory很好地解释了 recorder header中的n_ owned值的含义,因为这些记录并不包括在 Page Directory中。
需要牢记的是,B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过Page Directory再进行二叉查找。只不过二叉査找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。
为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等), 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值进行比较,看是否一致( checksum的比较需要通过 InnoDB的 checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性( not corrupted)。
在默认配置下, InnoDB存储引擎每次从磁盘读取一个页就会检测该页的完整性,即页是否发生 Corrupt,这就是通过 File trailer部分进行检测,而该部分的检测会有一定的开销。用户可以通过参数 innodb checksums来开启或关闭对这个页完整性的检查MySQL56.6版本开始新增了参数 innodb checksum algorithm,该参数用来控制检测 checksum函数的算法,默认值为crc32,可设置的值有: innodb、crc32、none、 strict innodb、 strict crc32、 strict none。
innodb为兼容之前版本 InnoDB页的 checksum检测方式,crc32为 MySQL5.6.6版本引进的新的 checksum算法,该算法较之前的 innodb有着较高的性能。但是若表中所有页的 checksum值都以 strict算法保存,那么低版本的 MySQL数据库将不能读取这些页。none表示不对页启用 checksum检查strict*正如其名,表示严格地按照设置的 checksum算法进行页的检测。因此若低版本 MySQL数据库升级到 MySQL566或之后的版本,启用 strict crc32将导致不能读取表中的页。启用 strict crc32方式是最快的方式,因为其不再对 innodb和crc32算法进行两次检测。故推荐使用该设置。若数据库从低版本升级而来,则需要进行 mysql_upgrade操作。