好的,我们开始。
今天我们来深入探讨 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_OFFSET
和FIL_PAGE_FILE_TYPE
用于唯一标识一个页及其类型。 - 页的链接:
FIL_PAGE_PREV
和FIL_PAGE_NEXT
用于将页链接成链表,例如,B+ 树的叶子节点页通过双向链表链接。 - 数据一致性:
FIL_PAGE_LSN
和FIL_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_RECS
、PAGE_FREE
、PAGE_GARBAGE
等字段用于管理页中的记录,包括记录的插入、删除和空间回收。 - 方向优化:
PAGE_DIRECTION
和PAGE_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 | 指向组内最大记录的偏移量。通过这个偏移量,我们可以快速定位到该组的最大记录,然后在该组内进行线性查找。 |
查找过程:
- 确定查找范围: 根据要查找的记录的键值,使用二分查找算法在 Page Directory 中找到对应的 Slot。这个 Slot 对应的组包含了可能包含目标记录的范围。
- 组内线性查找: 在确定的组内,从 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(用户记录)
用户记录存储了实际的数据行。 记录格式可以是 Compact
、Redundant
、Dynamic
或 Compressed
, 具体取决于 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 数据库专家的必备技能。掌握这些知识,将能更好地管理和优化数据库,为业务提供更稳定、高效的数据服务。