深入 `InnoDB` `数据页`(`Page`)的物理结构:`File Header`、`Page Header` 和`目录`的内部布局。

好的,我们开始。

今天我们来深入探讨 InnoDB 数据页的物理结构,主要聚焦于 File Header、Page Header 和目录这三个重要组成部分。理解这些结构对于深入了解 InnoDB 的存储机制至关重要,也能帮助我们更好地进行数据库调优和问题排查。

一、数据页概述

InnoDB 存储引擎将数据划分为页(Page)进行管理,它是 InnoDB 磁盘管理的最小单位。默认情况下,每个页的大小为 16KB。 数据页不仅存储了表中的数据,还包含了用于管理和组织数据的各种元数据信息。

二、File Header(文件头)

File Header 是每个数据页的第一个部分,占用 38 字节。它包含了页的通用信息,用于识别页的类型、页的校验和等。

字段名称 大小 (字节) 描述
FIL_PAGE_SPACE_OR_CHKSUM 4 页属于哪个表空间。如果是系统表空间,则存储的是校验和(checksum),用于验证页的完整性。
FIL_PAGE_OFFSET 4 页号,在表空间中的唯一标识。
FIL_PAGE_PREV 4 上一个页的页号。如果该页是表空间中的第一个页,则该字段值为FIL_NULL
FIL_PAGE_NEXT 4 下一个页的页号。如果该页是表空间中的最后一个页,则该字段值为FIL_NULL
FIL_PAGE_FILE_TYPE 4 页的类型。InnoDB 定义了多种页类型,例如:FIL_PAGE_INDEX(索引页)、FIL_PAGE_UNDO_LOG(Undo 日志页)、FIL_PAGE_INODE(Inode 页)等。
FIL_PAGE_FLUSH_LSN 8 最后一次刷新该页的 LSN(Log Sequence Number)。用于崩溃恢复。
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 归档日志号或表空间 ID。取决于 InnoDB 的配置。
FIL_PAGE_LSN 8 页的 LSN。表示该页上最近一次修改的日志序列号。

代码示例(伪代码,用于说明 File Header 的读取):

struct FileHeader {
  uint32_t space_or_checksum;
  uint32_t page_offset;
  uint32_t prev_page;
  uint32_t next_page;
  uint32_t file_type;
  uint64_t flush_lsn;
  uint32_t arch_log_no_or_space_id;
  uint64_t lsn;
};

// 假设 page_data 是指向数据页起始位置的指针
FileHeader* file_header = reinterpret_cast<FileHeader*>(page_data);

// 读取页号
uint32_t page_number = file_header->page_offset;

// 读取页类型
uint32_t page_type = file_header->file_type;

// 读取 LSN
uint64_t page_lsn = file_header->lsn;

// ... 更多操作 ...

重要性:

  • 页的识别: FIL_PAGE_OFFSETFIL_PAGE_FILE_TYPE 用于唯一标识一个页及其类型。
  • 页的链接: FIL_PAGE_PREVFIL_PAGE_NEXT 用于将页链接成链表,例如,B+ 树的叶子节点页通过双向链表链接。
  • 数据一致性: FIL_PAGE_LSNFIL_PAGE_FLUSH_LSN 用于确保数据的一致性和进行崩溃恢复。

三、Page Header(页头)

Page Header 占用 56 字节,包含了页的状态信息,例如页中记录的数量、空闲空间的位置等。

字段名称 大小 (字节) 描述
PAGE_N_DIR_SLOTS 2 该页中 Slot 的数量,也就是 Page Directory 中槽的数量。
PAGE_HEAP_TOP 2 堆顶指针。指向页面中堆空间的起始位置。在初始化时,该值等于 16384 (16KB) ,随着记录的插入而减小。
PAGE_N_HEAP 2 堆中记录的数量,包括最小和最大记录。
PAGE_FREE 2 指向可重用空间的起始地址。它是一个链表,链接了页面中已经被删除的记录所占用的空间,这些空间可以被新的记录使用。
PAGE_GARBAGE 2 页面中已经删除的记录所占用的总字节数。当该值达到一定阈值时,可能会触发页面碎片整理。
PAGE_LAST_INSERT 2 最后一个插入记录的位置。
PAGE_DIRECTION 2 记录插入的方向。用于优化记录的插入。
PAGE_N_DIRECTION 2 连续往同一个方向插入的记录数量。
PAGE_N_RECS 2 该页中记录的数量(不包括最小和最大记录)。
PAGE_MAX_TRX_ID 8 修改当前页的最大事务 ID。
PAGE_LSN 8 页的 LSN,同 File Header 中的 FIL_PAGE_LSN
PAGE_SPACE_OR_CHKSUM 4 页属于哪个表空间。如果是系统表空间,则存储的是校验和(checksum)。
PAGE_REC_CF_BITS 1 用于控制记录格式的位。
PAGE_REC_CF_NEXT_REC 1 指向下一条记录的偏移量。用于链接页面中的记录。
PAGE_FILE_PAGE_TYPE 4 页类型,同 File Header 中的 FIL_PAGE_FILE_TYPE
PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 归档日志号或表空间 ID,同 File Header 中的 FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

代码示例(伪代码,用于说明 Page Header 的读取):

struct PageHeader {
  uint16_t n_dir_slots;
  uint16_t heap_top;
  uint16_t n_heap;
  uint16_t free;
  uint16_t garbage;
  uint16_t last_insert;
  uint16_t direction;
  uint16_t n_direction;
  uint16_t n_recs;
  uint64_t max_trx_id;
  uint64_t lsn;
  uint32_t space_or_checksum;
  uint8_t rec_cf_bits;
  uint8_t rec_cf_next_rec;
  uint32_t file_page_type;
  uint32_t arch_log_no_or_space_id;
};

// 假设 page_data 是指向数据页起始位置的指针
PageHeader* page_header = reinterpret_cast<PageHeader*>(page_data + 38); // File Header 占用 38 字节

// 读取记录数量
uint16_t num_records = page_header->n_recs;

// 读取空闲空间起始地址
uint16_t free_space_offset = page_header->free;

// 读取槽的数量
uint16_t num_slots = page_header->n_dir_slots;

// ... 更多操作 ...

重要性:

  • 记录管理: PAGE_N_RECSPAGE_FREEPAGE_GARBAGE 等字段用于管理页中的记录,包括记录的插入、删除和空间回收。
  • 方向优化: PAGE_DIRECTIONPAGE_N_DIRECTION 用于优化记录的插入,提高插入效率。
  • 事务支持: PAGE_MAX_TRX_ID 用于支持事务,确保数据的隔离性和一致性。

四、Page Directory(页目录)

Page Directory 位于 Page Header 之后,用于加速记录的查找。它采用类似二分查找的思想,将页中的记录分成若干个组,每个组对应一个槽(Slot),槽中存储的是该组中最大记录的偏移量。

内部布局:

Page Directory 由多个 Slot 组成,每个 Slot 占用 2 个字节。Slot 的数量存储在 Page Header 的 PAGE_N_DIR_SLOTS 字段中。

字段名称 大小 (字节) 描述
Slot Offset 2 指向组内最大记录的偏移量。通过这个偏移量,我们可以快速定位到该组的最大记录,然后在该组内进行线性查找。

查找过程:

  1. 确定查找范围: 根据要查找的记录的键值,使用二分查找算法在 Page Directory 中找到对应的 Slot。这个 Slot 对应的组包含了可能包含目标记录的范围。
  2. 组内线性查找: 在确定的组内,从 Slot 指向的记录开始,向前进行线性查找,直到找到目标记录或者确定目标记录不存在。

代码示例(伪代码,用于说明 Page Directory 的查找):

// 假设 page_data 是指向数据页起始位置的指针
// 假设 record_key 是要查找的记录的键值

// 1. 读取 Page Header
PageHeader* page_header = reinterpret_cast<PageHeader*>(page_data + 38);
uint16_t num_slots = page_header->n_dir_slots;

// 2. 计算 Page Directory 的起始地址
uint8_t* page_directory = page_data + 38 + sizeof(PageHeader);

// 3. 二分查找 Page Directory
int low = 0;
int high = num_slots - 1;
int mid;
uint16_t slot_offset;

while (low <= high) {
  mid = (low + high) / 2;
  slot_offset = *reinterpret_cast<uint16_t*>(page_directory + mid * 2); // 每个 Slot 占用 2 字节

  // 假设 get_record_key(offset) 函数可以根据偏移量获取记录的键值
  KeyType slot_key = get_record_key(page_data + slot_offset);

  if (record_key == slot_key) {
    // 找到了,但是还要向前查找,因为可能存在键值相同的记录
    break;
  } else if (record_key < slot_key) {
    high = mid - 1;
  } else {
    low = mid + 1;
  }
}

// 4. 组内线性查找
uint16_t current_offset = slot_offset;
while (current_offset != 0) { // 0 表示到达最小记录
  KeyType current_key = get_record_key(page_data + current_offset);
  if (current_key == record_key) {
    // 找到了目标记录
    // ...
    break;
  }

  // 假设 get_previous_record_offset(offset) 函数可以根据偏移量获取前一条记录的偏移量
  current_offset = get_previous_record_offset(page_data + current_offset);
}

// 如果 current_offset 为 0,则表示没有找到目标记录
if (current_offset == 0) {
  // 目标记录不存在
  // ...
}

重要性:

  • 加速查找: Page Directory 通过将记录分组,并使用二分查找,显著提高了记录的查找效率,尤其是在数据量较大的情况下。
  • 优化查询性能: 通过 Page Directory,InnoDB 可以快速定位到可能包含目标记录的组,减少了需要扫描的记录数量,从而优化了查询性能。

五、User Records(用户记录)

用户记录存储了实际的数据行。 记录格式可以是 CompactRedundantDynamicCompressed, 具体取决于 MySQL 的版本和表的配置。 用户记录位于 Page Directory 之后,占据数据页的大部分空间。

六、Free Space(空闲空间)

Free Space 是数据页中未被使用的空间,用于存储新插入的记录。 当数据页中的记录被删除时,这些空间不会立即被释放,而是被标记为空闲空间,等待后续的记录插入。Free Space 通过链表的形式组织,Page Header 中的 PAGE_FREE 字段指向空闲空间链表的起始地址。

七、Infimum and Supremum Records(最小和最大记录)

每个数据页都包含两个特殊的记录:Infimum Record 和 Supremum Record。 Infimum Record 是页面中的最小记录,Supremum Record 是页面中的最大记录。 这两个记录不存储实际的用户数据,而是用于定义记录的边界,方便记录的插入和查找。

八、数据页结构总览

综合以上各个部分,一个典型的 InnoDB 数据页的物理结构如下:

+-----------------------+
|      File Header      | (38 bytes)
+-----------------------+
|      Page Header      | (56 bytes)
+-----------------------+
|     Infimum Record    |
+-----------------------+
|     Supremum Record   |
+-----------------------+
|    User Records       | (Variable size)
+-----------------------+
|     Free Space       | (Variable size)
+-----------------------+
|   Page Directory      | (Variable size)
+-----------------------+

九、总结:理解数据页结构的重要性

深入理解 InnoDB 数据页的物理结构,特别是 File Header、Page Header 和 Page Directory 的内部布局,对于数据库管理员和开发人员来说至关重要。 它可以帮助我们:

  • 更好地理解 InnoDB 的存储机制,包括数据的组织方式、记录的查找过程等。
  • 进行数据库性能调优,例如通过调整索引和查询语句,减少页的访问次数,提高查询效率。
  • 排查数据库问题,例如通过分析数据页的内容,诊断数据损坏、索引错误等问题。
  • 进行数据库恢复,例如通过分析 File Header 和 Page Header 中的 LSN 信息,恢复数据到一致的状态。

理解数据页结构是深入掌握 InnoDB 存储引擎的基础,也是成为一名优秀的 MySQL 数据库专家的必备技能。掌握这些知识,将能更好地管理和优化数据库,为业务提供更稳定、高效的数据服务。

发表回复

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