MySQL存储引擎之:`InnoDB`的`Page`结构:`行记录`、`Page Header`和`File Trailer`的内部布局。

MySQL InnoDB 存储引擎:Page 结构深度剖析

大家好,今天我们深入探讨 MySQL InnoDB 存储引擎的核心数据结构之一:Page。理解 Page 的内部布局,包括 行记录Page HeaderFile Trailer,对于优化数据库性能、排查问题至关重要。

Page 是 InnoDB 管理磁盘 I/O 的最小单元,默认大小为 16KB。所有数据都存储在 Page 中,例如表数据、索引数据等。通过高效地管理 Page,InnoDB 能够最大程度地减少磁盘 I/O,提升数据库性能。

1. Page 的整体结构

一个典型的 InnoDB Page 由以下几个部分组成:

部分 大小 (字节) 描述
File Header 38 包含 Page 的通用信息,例如 Page 类型、checksum 等。
System Records 26 包含两个特殊的记录:Infimum Record 和 Supremum Record。Infimum Record 是页中的最小记录,Supremum Record 是页中的最大记录。这两个记录不包含用户数据,主要用于简化记录的查找和排序。
User Records 可变 实际存储的用户数据行记录。
Free Space 可变 未使用的空间,用于后续插入新的记录。
Page Directory 可变 用于快速查找 User Records。 它是一个稀疏索引,将页中的记录分成多个槽,每个槽指向一个记录。通过 Page Directory 可以快速定位到目标记录所在的槽,从而减少查找的范围。
File Trailer 8 用于检测 Page 是否完整写入到磁盘。它包含 Page 的 checksum 和 LSN (Log Sequence Number) 的低 4 个字节。

2. 行记录 (Row Records)

行记录存储了表中的实际数据。InnoDB 使用行格式来存储数据,常见的行格式包括:REDUNDANTCOMPACTDYNAMICCOMPRESSED。从 MySQL 5.7 开始,默认的行格式是 DYNAMIC

行记录的结构取决于选择的行格式,这里我们以 DYNAMIC 行格式为例来讲解。DYNAMIC 行格式的主要特点是:对于长度超过一定阈值的列(默认是 768 字节),只存储前 768 字节,剩余部分存储在溢出页 (Overflow Page) 中。

一个 DYNAMIC 行记录通常包含以下部分:

部分 描述
record_type 1-3 bits,记录类型,例如:00 (普通记录),01 (索引记录),10 (Infimum Record),11 (Supremum Record)。
delete_mask 1 bit,删除标记。如果设置为 1,表示该记录已被删除。
next_record 16 bits,指向下一条记录的偏移量。通过 next_record,可以将页中的记录连接成一个链表。
n_owned 4 bits,Page Directory 中每个槽拥有的记录数。
heap_no 13 bits,堆号。用于标识记录在页中的位置。
null_bitmap 可变长度,NULL 值标记位图。 如果某个列的值为 NULL,则在 null_bitmap 中对应的位设置为 1。
record_header 可变长度,记录头信息。 包括变长字段长度列表、事务 ID、回滚指针等。
column_1_data 列 1 的数据。
column_2_data 列 2 的数据。

代码示例:模拟行记录的结构 (Python)

class RowRecord:
    def __init__(self, record_type, delete_mask, next_record, n_owned, heap_no, null_bitmap, record_header, column_data):
        self.record_type = record_type
        self.delete_mask = delete_mask
        self.next_record = next_record
        self.n_owned = n_owned
        self.heap_no = heap_no
        self.null_bitmap = null_bitmap
        self.record_header = record_header
        self.column_data = column_data

    def __repr__(self):
        return f"RowRecord(record_type={self.record_type}, delete_mask={self.delete_mask}, next_record={self.next_record}, n_owned={self.n_owned}, heap_no={self.heap_no}, null_bitmap={self.null_bitmap}, record_header={self.record_header}, column_data={self.column_data})"

# 示例数据
record1 = RowRecord(
    record_type="00",
    delete_mask="0",
    next_record=100,
    n_owned=2,
    heap_no=5,
    null_bitmap=[0, 1, 0], # 第二列为 NULL
    record_header={"transaction_id": 123, "rollback_pointer": 456},
    column_data=["value1", None, "value3"] # 第二列的值为 None
)

print(record1)

这个 Python 示例只是为了说明行记录的结构,实际的 InnoDB 行记录结构比这复杂得多,并且是二进制格式。

变长字段的存储:

对于 VARCHARTEXT 等变长字段,InnoDB 会在 record_header 中存储一个变长字段长度列表。该列表记录了每个变长字段的实际长度。这样,InnoDB 就可以根据长度列表来读取变长字段的数据。

溢出页 (Overflow Page):

如果某个列的数据长度超过阈值(默认是 768 字节),InnoDB 会将该列的数据存储在溢出页中。在行记录中,只存储该列的前 768 字节,以及指向溢出页的指针。通过溢出页,InnoDB 可以存储非常大的数据,而不会影响页的整体结构。

3. Page Header

Page Header 包含 Page 的元数据信息,对于管理 Page 非常重要。

Page Header 的结构如下:

字段 大小 (字节) 描述
PAGE_LSN 8 Page 的 LSN (Log Sequence Number)。 LSN 是一个单调递增的数字,用于标识 Page 的版本。 通过 LSN 可以判断 Page 是否是最新的版本,以及在崩溃恢复时进行数据恢复。
PAGE_ID 4 Page 的 ID。 在一个表空间中,每个 Page 都有一个唯一的 ID。
PAGE_PREV 4 指向前一个 Page 的 ID。 用于将 Page 连接成一个双向链表。
PAGE_NEXT 4 指向后一个 Page 的 ID。 用于将 Page 连接成一个双向链表。
PAGE_SPACE_OR_CHKSUM 4 表空间 ID 或校验和。取决于具体情况,早期版本是表空间ID,后期版本是校验和。
PAGE_DIRECTION 2 最近插入记录的方向。 用于优化记录的插入。
PAGE_N_RECS 2 Page 中记录的数量。
PAGE_MAX_TRX_ID 8 Page 中最大的事务 ID。
PAGE_HEAP_TOP 2 当前已分配堆空间的最大偏移量。
PAGE_FREE 2 第一个被删除记录的偏移量。 通过 PAGE_FREE,可以将被删除的记录连接成一个链表,方便后续的记录插入。
PAGE_LAST_INSERT 2 最后插入记录的偏移量。
PAGE_GARBAGE 2 Page 中被删除记录占用的字节数。

代码示例:模拟 Page Header 的结构 (Python)

class PageHeader:
    def __init__(self, page_lsn, page_id, page_prev, page_next, page_space_or_chksum, page_direction, page_n_recs, page_max_trx_id, page_heap_top, page_free, page_last_insert, page_garbage):
        self.page_lsn = page_lsn
        self.page_id = page_id
        self.page_prev = page_prev
        self.page_next = page_next
        self.page_space_or_chksum = page_space_or_chksum
        self.page_direction = page_direction
        self.page_n_recs = page_n_recs
        self.page_max_trx_id = page_max_trx_id
        self.page_heap_top = page_heap_top
        self.page_free = page_free
        self.page_last_insert = page_last_insert
        self.page_garbage = page_garbage

    def __repr__(self):
        return f"PageHeader(page_lsn={self.page_lsn}, page_id={self.page_id}, page_prev={self.page_prev}, page_next={self.page_next}, page_space_or_chksum={self.page_space_or_chksum}, page_direction={self.page_direction}, page_n_recs={self.page_n_recs}, page_max_trx_id={self.page_max_trx_id}, page_heap_top={self.page_heap_top}, page_free={self.page_free}, page_last_insert={self.page_last_insert}, page_garbage={self.page_garbage})"

# 示例数据
header = PageHeader(
    page_lsn=1234567890,
    page_id=10,
    page_prev=9,
    page_next=11,
    page_space_or_chksum=1,
    page_direction=2,
    page_n_recs=50,
    page_max_trx_id=999,
    page_heap_top=8192,
    page_free=100,
    page_last_insert=200,
    page_garbage=500
)

print(header)

这个 Python 示例同样只是为了说明 Page Header 的结构,实际的 InnoDB Page Header 是二进制格式,存储在 Page 的头部。

Page 的链表结构:

通过 PAGE_PREVPAGE_NEXT 字段,可以将 Page 连接成一个双向链表。这种链表结构用于管理 Page,例如在 B+ 树索引中,叶子节点 Page 会通过链表连接起来,方便范围查询。

LSN (Log Sequence Number):

PAGE_LSN 记录了 Page 的 LSN。 LSN 是一个单调递增的数字,用于标识 Page 的版本。InnoDB 使用 LSN 来保证数据的一致性和持久性。在崩溃恢复时,InnoDB 会根据 LSN 来判断 Page 是否是最新的版本,以及进行数据恢复。

4. File Trailer

File Trailer 位于 Page 的末尾,用于检测 Page 是否完整写入到磁盘。

File Trailer 的结构如下:

字段 大小 (字节) 描述
FIL_PAGE_END_LSN 4 Page 的 LSN 的低 4 个字节。
FIL_PAGE_FILE_CHKSUM 4 Page 的 checksum。 用于校验 Page 的数据是否损坏。 在将 Page 写入磁盘时,InnoDB 会计算 Page 的 checksum,并将 checksum 存储在 File Trailer 中。 在读取 Page 时,InnoDB 会重新计算 Page 的 checksum,并与 File Trailer 中的 checksum 进行比较。 如果两个 checksum 不一致,则表示 Page 的数据可能已损坏。

代码示例:模拟 File Trailer 的结构 (Python)

class FileTrailer:
    def __init__(self, fil_page_end_lsn, fil_page_file_chksum):
        self.fil_page_end_lsn = fil_page_end_lsn
        self.fil_page_file_chksum = fil_page_file_chksum

    def __repr__(self):
        return f"FileTrailer(fil_page_end_lsn={self.fil_page_end_lsn}, fil_page_file_chksum={self.fil_page_file_chksum})"

# 示例数据
trailer = FileTrailer(
    fil_page_end_lsn=1234,
    fil_page_file_chksum=5678
)

print(trailer)

这个 Python 示例同样只是为了说明 File Trailer 的结构,实际的 InnoDB File Trailer 是二进制格式,存储在 Page 的末尾。

Checksum 校验:

FIL_PAGE_FILE_CHKSUM 字段存储了 Page 的 checksum。InnoDB 使用 checksum 来检测 Page 的数据是否损坏。在将 Page 写入磁盘时,InnoDB 会计算 Page 的 checksum,并将 checksum 存储在 File Trailer 中。在读取 Page 时,InnoDB 会重新计算 Page 的 checksum,并与 File Trailer 中的 checksum 进行比较。如果两个 checksum 不一致,则表示 Page 的数据可能已损坏。

LSN 的低 4 字节:

FIL_PAGE_END_LSN 字段存储了 Page 的 LSN 的低 4 个字节。虽然只存储了低 4 个字节,但在大多数情况下,仍然可以用于判断 Page 是否是最新的版本。

5. Page Directory

Page Directory 用于加速在 Page 中查找记录。 它是一个稀疏索引,将页中的记录分成多个槽,每个槽指向一个记录。

Page Directory 的工作原理如下:

  1. 将页中的记录分成多个槽。
  2. 每个槽指向一个记录。
  3. 每个槽拥有的记录数量存储在记录的 n_owned 字段中。
  4. 通过二分查找,可以快速定位到目标记录所在的槽。
  5. 在槽中,可以通过链表遍历找到目标记录。

通过 Page Directory,可以避免在整个 Page 中进行线性查找,从而提高查找效率。

6. InnoDB Page 的类型

InnoDB 定义了多种 Page 类型,每种类型用于存储不同的数据。常见的 Page 类型包括:

Page 类型 描述
FIL_PAGE_INDEX 索引页。 用于存储 B+ 树索引的节点。
FIL_PAGE_UNDO_LOG Undo 日志页。 用于存储 Undo 日志。
FIL_PAGE_INODE INODE 页。 用于存储文件系统的 INODE 信息。
FIL_PAGE_IBUF_FREE_LIST IBUF 空闲列表页。 用于管理 IBUF (Insert Buffer) 的空闲空间。
FIL_PAGE_TYPE_ALLOC 类型分配页。 用于分配 Page 类型。
FIL_PAGE_SDI 序列化数据交换(SDI)页。用于存储数据字典信息,例如表定义、列定义等。这些信息以序列化的格式存储,方便在不同的 MySQL 实例之间进行数据交换和复制。SDI页是在 MySQL 8.0 中引入的,用于替代以前的 INFORMATION_SCHEMA 视图,提供更高效和可靠的数据字典访问方式。

不同的 Page 类型有不同的结构和用途。

Page 的结构决定了存储效率

深入理解 InnoDB Page 的内部布局,包括 行记录Page HeaderFile Trailer,对于优化数据库性能至关重要。 Page 结构的设计直接影响了数据的存储效率、查找效率和数据一致性。 理解行格式、溢出页、Page Directory、LSN 和 Checksum 等概念,可以帮助我们更好地理解 InnoDB 的工作原理,从而更好地优化数据库性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注