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

InnoDB Page 结构深度剖析:行记录、Page Header 与 File Trailer

大家好,今天我们深入探讨 MySQL InnoDB 存储引擎中最基础也是最重要的概念之一:Page 结构。理解 Page 结构对于优化数据库性能、排查问题以及更深入地理解 InnoDB 的工作原理至关重要。我们将详细分析 Page 内部的行记录、Page Header 和 File Trailer 的布局,并结合代码示例进行讲解。

1. Page 的概念

InnoDB 将数据存储在磁盘上,但每次读写操作并非直接针对磁盘扇区,而是以 Page 为单位。Page 是 InnoDB 存储的最基本单元,默认大小为 16KB。这可以减少磁盘 I/O 次数,提高性能。一个 Page 可以存储多个行记录,以及其他一些元数据信息。

2. Page 的整体结构

一个 InnoDB Page 主要由以下几个部分组成:

  • File Header (38 字节): 包含 Page 的一些通用信息,如 Page 类型、校验和等。
  • Page Header (56 字节): 包含 Page 的状态信息,如 Page 中空闲空间大小、记录数量等。
  • Infimum + Supremum Records (至少 26 字节): 两个虚拟记录,分别表示 Page 中最小和最大的记录。
  • User Records (可变): 实际存储的行记录。
  • Free Space (可变): Page 中未使用的空间,用于存放新的记录。
  • Page Directory (可变): 记录槽(slot)的目录,用于快速查找记录。
  • File Trailer (8 字节): 用于校验 Page 的完整性。

3. 行记录 (User Records)

行记录是 Page 中存储数据的核心部分。InnoDB 支持多种行记录格式,如 Compact 和 Dynamic。我们以 Compact 格式为例进行讲解。

3.1 Compact 行记录格式

Compact 格式的行记录主要包含以下部分:

  • Record Header (5 字节): 包含记录的一些元数据信息,如记录的删除标记、记录类型、记录的下一个记录的偏移量等。
  • Variable-Length Columns Length (可变): 存储变长列的长度。如果所有列都是定长,则此部分不存在。
  • NULL Flags (可变): 存储 NULL 值的标记。如果所有列都不允许为 NULL,则此部分不存在。
  • Record Data (可变): 实际存储的列数据。

3.1.1 Record Header 详解

Record Header 的 5 个字节包含以下信息:

位偏移 长度 (bit) 描述
0-0 1 delete_mask: 标记记录是否被删除。1 表示已删除,0 表示未删除。
1-1 1 min_rec_mask: 标记记录是否为 B+ 树的最小记录。
2-4 3 n_owned: 记录拥有的记录数。只有在 Page Directory 的 slot 中存储的记录才拥有记录。
5-6 2 heap_no: 记录在堆中的位置。0 表示 Infimum Record, 1 表示 Supremum Record。其它值表示 User Record。
7-7 1 record_type: 记录类型。0 表示普通记录,1 表示 B+ 树非叶子节点记录,2 表示 Infimum Record, 3 表示 Supremum Record。
8-15 8 next_record: 指向下一条记录的偏移量。如果当前记录是 Page 中的最后一条记录,则 next_record 的值为 0。 它描述的是当前记录的真实偏移量,而不是逻辑偏移量。也就是说,它描述的是当前记录在 Page 中的物理位置与下一条记录在 Page 中的物理位置之间的距离。

3.1.2 代码示例 (伪代码)

假设我们有一张表 user,包含 id (INT, NOT NULL, PRIMARY KEY), name (VARCHAR(20), NOT NULL), age (INT, NULL) 三个字段。插入一条记录 (1, 'Alice', 30)

// 假设 Page 已经分配,并且有足够的空间
// 记录数据
id = 1;
name = "Alice";
age = 30;

// 计算变长列长度 (name)
name_length = strlen(name);

// 构建 Record Header
delete_mask = 0; // 未删除
min_rec_mask = 0; // 不是最小记录
n_owned = 0;      // 不拥有记录
heap_no = 2;      // User Record
record_type = 0;  // 普通记录
next_record = 0;  // 假设是 Page 中的最后一条记录

// 将 Record Header 各个部分组合成一个字节数组
record_header = pack_record_header(delete_mask, min_rec_mask, n_owned, heap_no, record_type, next_record);

// 构建 Variable-Length Columns Length (只有 name 是变长列)
variable_length_columns_length = pack_length(name_length);

// 构建 NULL Flags (age 可以为 NULL)
null_flags = 0x00; // age 不为 NULL

// 构建 Record Data
record_data = pack_int(id);
record_data += name;
record_data += pack_int(age);

// 将各个部分组合成完整的行记录
row_record = record_header + variable_length_columns_length + null_flags + record_data;

// 将行记录写入 Page
write_to_page(page, row_record);

3.2 Infimum 和 Supremum 记录

这两个记录是 Page 中特殊的虚拟记录,用于定义 Page 中记录的范围。Infimum 记录是 Page 中最小的记录,Supremum 记录是 Page 中最大的记录。它们不存储实际的用户数据,而是用于优化记录的查找。

4. Page Header

Page Header 存储 Page 的状态信息,对于理解 InnoDB 的工作方式至关重要。Page Header 占用 56 字节,包含以下信息:

偏移量 长度 (字节) 描述
0 2 PAGE_DIRECTION: 最近插入记录的方向。如果 Page 已经满了,这个值就没什么意义了。
2 2 PAGE_N_RECS: Page 中的记录数 (不包括 Infimum 和 Supremum 记录)。
4 2 PAGE_MAX_TRX_ID: 修改 Page 的最大事务 ID。
6 2 PAGE_HEAP_TOP: 堆顶指针,指向 Page 中第一个未使用的字节。
8 2 PAGE_N_HEAP: 堆中记录的数量,包括 Infimum 和 Supremum 记录。
10 2 PAGE_FREE: 指向 Page 中第一个被删除的记录。
12 2 PAGE_GARBAGE: Page 中被删除的记录所占用的字节数。
14 4 PAGE_LAST_INSERT: 最后插入记录的位置。
18 2 PAGE_N_DIRECTION: 最近插入记录的方向数量。
20 8 PAGE_LATCH: Page 的锁信息。
28 4 PAGE_INDEX_ID: 索引 ID。
32 2 PAGE_N_NO_CLEAN: 未清理的记录数量。
34 2 PAGE_N_OLD_VERSIONS: 旧版本记录数量。
36 8 PAGE_SPACE_OR_CHECKSUM: 页面的空间 ID 或校验和。在较新的 MySQL 版本中,此字段用于存储校验和。
44 4 PAGE_LSN: Page 的日志序列号 (Log Sequence Number)。
48 4 PAGE_MAGIC_N: Page 的魔数,用于验证 Page 的类型。
52 4 PAGE_FILE_FLUSH_LSN: 文件刷新 LSN。

4.1 Page Header 的作用

  • 记录管理: PAGE_N_RECS, PAGE_HEAP_TOP, PAGE_FREE, PAGE_GARBAGE 等字段用于管理 Page 中的记录,包括记录的数量、空闲空间、已删除记录等。
  • 事务管理: PAGE_MAX_TRX_ID, PAGE_LSN 等字段用于事务管理,保证事务的 ACID 特性。
  • 并发控制: PAGE_LATCH 用于实现 Page 的并发控制,防止数据竞争。
  • 索引管理: PAGE_INDEX_ID 用于标识 Page 所属的索引。
  • 数据恢复: PAGE_LSN, PAGE_FILE_FLUSH_LSN 用于数据恢复,保证数据的持久性。

4.2 代码示例 (伪代码)

// 假设已经读取了 Page Header 的 56 个字节
byte[] page_header_bytes = read_page_header(page);

// 解析 Page Header
page_direction = bytes_to_short(page_header_bytes, 0);
page_n_recs = bytes_to_short(page_header_bytes, 2);
page_max_trx_id = bytes_to_short(page_header_bytes, 4);
page_heap_top = bytes_to_short(page_header_bytes, 6);
page_n_heap = bytes_to_short(page_header_bytes, 8);
page_free = bytes_to_short(page_header_bytes, 10);
page_garbage = bytes_to_short(page_header_bytes, 12);
page_last_insert = bytes_to_int(page_header_bytes, 14);
page_n_direction = bytes_to_short(page_header_bytes, 18);
page_latch = bytes_to_long(page_header_bytes, 20);
page_index_id = bytes_to_int(page_header_bytes, 28);
page_n_no_clean = bytes_to_short(page_header_bytes, 32);
page_n_old_versions = bytes_to_short(page_header_bytes, 34);
page_space_or_checksum = bytes_to_long(page_header_bytes, 36);
page_lsn = bytes_to_int(page_header_bytes, 44);
page_magic_n = bytes_to_int(page_header_bytes, 48);
page_file_flush_lsn = bytes_to_int(page_header_bytes, 52);

// 打印 Page Header 的信息
print("Page Direction: " + page_direction);
print("Page Number of Records: " + page_n_recs);
print("Page Maximum Transaction ID: " + page_max_trx_id);
print("Page Heap Top: " + page_heap_top);
print("Page Number of Heaps: " + page_n_heap);
print("Page Free: " + page_free);
print("Page Garbage: " + page_garbage);
print("Page Last Insert: " + page_last_insert);
print("Page Number of Directions: " + page_n_direction);
print("Page Latch: " + page_latch);
print("Page Index ID: " + page_index_id);
print("Page Number of No Clean Records: " + page_n_no_clean);
print("Page Number of Old Versions: " + page_n_old_versions);
print("Page Space or Checksum: " + page_space_or_checksum);
print("Page LSN: " + page_lsn);
print("Page Magic Number: " + page_magic_n);
print("Page File Flush LSN: " + page_file_flush_lsn);

5. Page Directory

Page Directory 用于快速查找 Page 中的记录。它将 Page 中的记录分成多个槽(slot),每个槽指向该槽中最后一个记录的地址。通过 Page Directory,可以减少查找记录的次数,提高查询性能。

5.1 Page Directory 的工作原理

当需要查找一条记录时,首先通过二分查找确定记录所在的槽,然后从槽指向的记录开始,依次遍历槽中的记录,直到找到目标记录。

5.2 代码示例 (伪代码)

// 假设已经读取了 Page Directory
byte[] page_directory_bytes = read_page_directory(page);

// 计算槽的数量
number_of_slots = calculate_number_of_slots(page_directory_bytes);

// 二分查找槽
target_record_key = ...; // 要查找的记录的键值
slot_index = binary_search_slot(page_directory_bytes, number_of_slots, target_record_key);

// 获取槽指向的记录地址
record_address = get_record_address_from_slot(page_directory_bytes, slot_index);

// 从槽指向的记录开始,依次遍历槽中的记录
current_record = read_record(page, record_address);
while (current_record != null) {
    if (current_record.key == target_record_key) {
        // 找到目标记录
        return current_record;
    }
    current_record = read_next_record(page, current_record);
}

// 未找到目标记录
return null;

6. Free Space

Free Space 是 Page 中未使用的空间,用于存放新的记录。当需要插入新的记录时,InnoDB 会从 Free Space 中分配空间。当删除记录时,被删除的记录的空间会被添加到 Free Space 中。

7. File Trailer

File Trailer 位于 Page 的末尾,占用 8 字节,用于校验 Page 的完整性。它包含 Page 的校验和 (checksum) 和 LSN 的一部分。

7.1 File Trailer 的作用

当从磁盘读取 Page 时,InnoDB 会计算 Page 的校验和,并与 File Trailer 中存储的校验和进行比较。如果两个校验和不一致,说明 Page 数据损坏,需要进行修复。

7.2 代码示例 (伪代码)

// 假设已经读取了 Page 的所有数据
byte[] page_data = read_page(page_address);

// 计算 Page 的校验和
calculated_checksum = calculate_checksum(page_data, 0, page_data.length - 8); // 排除 File Trailer

// 读取 File Trailer 中的校验和
file_trailer_checksum = bytes_to_int(page_data, page_data.length - 8);

// 比较校验和
if (calculated_checksum != file_trailer_checksum) {
    // Page 数据损坏
    print("Page data is corrupted!");
} else {
    // Page 数据完整
    print("Page data is valid.");
}

8. InnoDB Page 结构的重要性

理解 InnoDB Page 结构对于以下方面至关重要:

  • 性能优化: 通过理解 Page 的内部布局,可以优化 SQL 查询,减少磁盘 I/O 次数,提高查询性能。
  • 问题排查: 当数据库出现问题时,可以通过分析 Page 的内容,快速定位问题。
  • 数据恢复: 当数据库出现故障时,可以通过 Page 中的 LSN 信息,进行数据恢复。
  • 存储引擎设计: 理解 InnoDB Page 结构,可以帮助我们更好地理解存储引擎的设计思想。

9. 总结:深入理解 Page 结构,优化数据库性能

我们深入探讨了 InnoDB 的 Page 结构,包括行记录的 Compact 格式、Page Header 的各个字段以及 File Trailer 的校验机制。 通过理解 Page 结构,我们可以更好地理解 InnoDB 的工作原理,并以此为基础优化数据库性能,排查问题和进行数据恢复。掌握这些知识点对于成为一名优秀的 MySQL DBA 或数据库开发人员至关重要。

发表回复

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