`InnoDB`的`Page`结构:`行记录`、`Page Header`和`File Trailer`的`字节`级`解析`。

InnoDB Page 结构深度解析:行记录、Page Header 和 File Trailer

大家好,今天我们来深入探讨 InnoDB 存储引擎中 Page 的内部结构,重点关注行记录、Page Header 和 File Trailer 这三个关键组成部分。我们将从字节级别的角度来解析它们,理解它们在数据存储和完整性保障中的作用。

1. InnoDB Page 概览

在深入到具体的结构之前,我们先来了解一下 InnoDB Page 的基本概念。InnoDB 存储引擎以 Page (也称为数据页) 为最小的磁盘管理单元。每个 Page 的大小默认为 16KB,可以通过 innodb_page_size 参数进行配置。Page 包含多种类型,例如数据页、索引页、undo 页、系统页等。我们今天主要关注数据页,它用于存储表中的实际数据。

一个典型的 InnoDB 数据页的结构如下:

结构名称 大小 (字节) 描述
File Header 38 包含 Page 的通用信息,例如 Page 类型、Page 号、checksum 等。
Page Header 56 包含 Page 内部的管理信息,例如 Page 中空闲空间的大小、Page 中记录的数量、指向 Page 中最小和最大记录的指针等。
Infimum + Supremum 26 两个虚拟记录,分别表示 Page 中最小和最大的记录。它们不存储实际数据,用于简化记录的查找过程。
User Records 变长 存储实际的用户数据,也就是表中的行记录。
Free Space 变长 Page 中剩余的空闲空间,用于存储新的记录。
File Trailer 8 包含 Page 的 checksum 和 LSN (Log Sequence Number)。用于检测 Page 的数据是否损坏,以及在崩溃恢复时确定 Page 的状态。

2. 行记录 (User Records)

行记录是 Page 中存储实际数据的地方。InnoDB 使用一种称为 Compact 行格式来存储数据。Compact 行格式旨在减少存储空间,提高性能。

一个 Compact 行格式的记录包含以下部分:

结构名称 大小 (字节) 描述
Record Header 5 包含记录的控制信息,例如记录是否被删除、记录类型、下一条记录的偏移量等。
Next Record Offset 2 指向下一条记录的偏移量,用于在 Page 中查找记录。
1 NULL flag bits 变长 如果表中存在允许为 NULL 的列,则使用该字段来标记哪些列的值为 NULL。
Variable-length column data length 变长 如果列的数据类型是变长的,例如 VARCHAR、TEXT、BLOB 等,则使用该字段来存储列数据的长度。
Column 1 data 变长 存储列 1 的实际数据。
Column 2 data 变长 存储列 2 的实际数据。

2.1 Record Header

Record Header 是行记录中最关键的部分之一,它包含了记录的元数据。Record Header 的 5 个字节分别表示以下信息:

  • delete_mask (1 bit): 标记记录是否被删除。如果该位为 1,则表示记录已被删除。
  • min_rec_mask (1 bit): 标记记录是否为 B+ 树索引的最小记录。
  • n_owned (4 bits): 表示该记录拥有的记录数,仅在 B+ 树索引页中使用。
  • heap_no (13 bits): 表示记录在 Page 中的堆位置。
  • record_type (3 bits): 表示记录的类型,例如普通记录、B+ 树索引记录等。
  • next_record (16 bits): 表示下一条记录的偏移量,用于在 Page 中查找记录。

2.2 NULL flag bits

如果表中有允许为 NULL 的列,那么每个记录都需要一个 NULL flag bits 字段来标记哪些列的值为 NULLNULL flag bits 字段的长度取决于表中允许为 NULL 的列的数量。

2.3 Variable-length column data length

对于变长列,例如 VARCHARTEXTBLOB 等,我们需要存储列数据的长度。 InnoDB 使用不同的方法来存储长度,具体取决于长度的值。

  • 如果长度小于 256 字节,则使用 1 个字节来存储长度。
  • 如果长度大于等于 256 字节,则使用 2 个字节来存储长度。

2.4 代码示例:解析行记录

为了更好地理解行记录的结构,我们可以编写一个简单的 Python 脚本来解析行记录。以下是一个示例脚本,可以解析 Compact 行格式的记录:

import struct

def parse_record_header(record_header_bytes):
    """
    解析 Record Header
    """
    record_header = struct.unpack("<B", record_header_bytes)[0]

    delete_mask = (record_header >> 7) & 1
    min_rec_mask = (record_header >> 6) & 1
    n_owned = (record_header >> 2) & 0x0F
    record_type = record_header & 0x03

    return {
        "delete_mask": delete_mask,
        "min_rec_mask": min_rec_mask,
        "n_owned": n_owned,
        "record_type": record_type
    }

def parse_row_record(record_bytes, column_types):
    """
    解析行记录
    record_bytes: 行记录的字节数据
    column_types: 列类型列表,例如 ["INT", "VARCHAR(255)", "DATE"]
    """
    offset = 0

    # 解析 Record Header
    record_header_bytes = record_bytes[offset:offset + 1]
    offset += 1
    record_header = parse_record_header(record_header_bytes)
    print(f"Record Header: {record_header}")

    # 解析 Next Record Offset
    next_record_offset = struct.unpack("<H", record_bytes[offset:offset + 2])[0]
    offset += 2
    print(f"Next Record Offset: {next_record_offset}")

    # 假设存在 NULL flag bits
    null_flag_bits_length = (len(column_types) + 7) // 8  # 计算 NULL flag bits 的长度
    null_flag_bits = record_bytes[offset:offset + null_flag_bits_length]
    offset += null_flag_bits_length
    print(f"NULL Flag Bits: {null_flag_bits.hex()}")

    # 解析列数据
    column_values = []
    for i, column_type in enumerate(column_types):
        if (null_flag_bits[i // 8] >> (i % 8)) & 1:
            # 列的值为 NULL
            column_values.append(None)
            print(f"Column {i+1}: NULL")
            continue

        if column_type.startswith("VARCHAR"):
            # 解析 VARCHAR 列
            length_byte = record_bytes[offset:offset + 1]
            length = struct.unpack("<B", length_byte)[0]
            offset += 1
            value = record_bytes[offset:offset + length].decode("utf-8")
            offset += length
            column_values.append(value)
            print(f"Column {i+1} (VARCHAR): {value}")

        elif column_type == "INT":
            # 解析 INT 列
            value = struct.unpack("<i", record_bytes[offset:offset + 4])[0]
            offset += 4
            column_values.append(value)
            print(f"Column {i+1} (INT): {value}")

        elif column_type == "DATE":
            # 解析 DATE 列(这里简化处理,假设为 YYYY-MM-DD 格式的字符串)
            length = 10  # YYYY-MM-DD 长度为 10
            value = record_bytes[offset:offset + length].decode("utf-8")
            offset += length
            column_values.append(value)
            print(f"Column {i+1} (DATE): {value}")
        else:
            print(f"Unsupported column type: {column_type}")
            return None

    return column_values

# 示例用法
# 假设有如下表结构:
# CREATE TABLE test_table (
#     id INT,
#     name VARCHAR(255),
#     created_at DATE
# );
#
#  假设有一行记录的字节数据如下(需要根据实际情况修改)
record_bytes = bytes.fromhex("00650000047465737400000000323032332d31302d3236") # 示例数据,需要根据实际情况修改

column_types = ["INT", "VARCHAR(255)", "DATE"] # 表的列类型

parsed_values = parse_row_record(record_bytes, column_types)

if parsed_values:
    print(f"Parsed Values: {parsed_values}")

注意:

  • 这只是一个简单的示例,实际的行记录结构可能更复杂。
  • 需要根据表的实际结构和数据来修改代码。
  • InnoDB 使用了多种优化技术来存储数据,例如前缀压缩、差分存储等。

3. Page Header

Page Header 包含 Page 内部的管理信息。它的大小为 56 字节,包含以下字段:

字段名称 大小 (字节) 描述
PAGE_DIRECTION 2 最后一次插入记录的方向。
PAGE_N_DIRECTION 2 在同一个方向插入记录的次数。
PAGE_N_RECS 2 Page 中记录的数量 (不包括 Infimum 和 Supremum 记录)。
PAGE_MAX_TRX_ID 8 修改当前 Page 的最大的事务 ID。
PAGE_LEVEL 2 Page 的层级,仅在 B+ 树索引页中使用。
PAGE_INDEX_ID 8 索引 ID。
PAGE_B_LEFT 4 左兄弟 Page 的 Page Number。
PAGE_B_RIGHT 4 右兄弟 Page 的 Page Number。
PAGE_N_HEAP_REC 2 堆中记录的数量。
PAGE_FREE 4 指向 Free Space 起始位置的指针。
PAGE_GARBAGE 2 Page 中已删除记录占用的字节数。
PAGE_LAST_INSERT 2 最后一次插入记录的位置。
PAGE_N_BITS 2 用于记录 Page 中记录的位图,用于快速查找空闲空间。

Page Header 中的信息对于 InnoDB 的内部管理非常重要。例如,PAGE_N_RECS 用于快速获取 Page 中记录的数量,PAGE_FREE 用于快速找到 Free Space 的起始位置,PAGE_GARBAGE 用于判断是否需要进行 Page 合并。

4. File Trailer

File Trailer 用于检测 Page 的数据是否损坏,以及在崩溃恢复时确定 Page 的状态。它的大小为 8 字节,包含以下字段:

字段名称 大小 (字节) 描述
CHECKSUM 4 Page 的 checksum 值。InnoDB 在将 Page 写入磁盘之前会计算 checksum 值,并将它存储在 File Trailer 中。在读取 Page 时,InnoDB 会重新计算 checksum 值,并与 File Trailer 中的 checksum 值进行比较。如果两个 checksum 值不一致,则表示 Page 的数据已损坏。
LSN 4 Page 的 LSN (Log Sequence Number)。LSN 是一个单调递增的数字,用于标识每个 Page 的版本。InnoDB 使用 LSN 来进行崩溃恢复。在崩溃恢复时,InnoDB 会比较磁盘上的 Page 的 LSN 和 Redo Log 中的 LSN。如果磁盘上的 Page 的 LSN 小于 Redo Log 中的 LSN,则表示该 Page 需要进行恢复。

File Trailer 对于 InnoDB 的数据完整性至关重要。通过 checksum 和 LSN,InnoDB 可以在数据损坏或崩溃时进行检测和恢复。

5. 代码示例:解析 Page Header 和 File Trailer

import struct

def parse_page_header(page_header_bytes):
    """
    解析 Page Header
    """
    page_header = {}

    page_header["PAGE_DIRECTION"] = struct.unpack("<H", page_header_bytes[0:2])[0]
    page_header["PAGE_N_DIRECTION"] = struct.unpack("<H", page_header_bytes[2:4])[0]
    page_header["PAGE_N_RECS"] = struct.unpack("<H", page_header_bytes[4:6])[0]
    page_header["PAGE_MAX_TRX_ID"] = struct.unpack("<q", page_header_bytes[6:14])[0] # 使用 "q" 解析 8 字节的 long long
    page_header["PAGE_LEVEL"] = struct.unpack("<H", page_header_bytes[14:16])[0]
    page_header["PAGE_INDEX_ID"] = struct.unpack("<q", page_header_bytes[16:24])[0] # 使用 "q" 解析 8 字节的 long long
    page_header["PAGE_B_LEFT"] = struct.unpack("<I", page_header_bytes[24:28])[0]
    page_header["PAGE_B_RIGHT"] = struct.unpack("<I", page_header_bytes[28:32])[0]
    page_header["PAGE_N_HEAP_REC"] = struct.unpack("<H", page_header_bytes[32:34])[0]
    page_header["PAGE_FREE"] = struct.unpack("<I", page_header_bytes[34:38])[0]
    page_header["PAGE_GARBAGE"] = struct.unpack("<H", page_header_bytes[38:40])[0]
    page_header["PAGE_LAST_INSERT"] = struct.unpack("<H", page_header_bytes[40:42])[0]
    page_header["PAGE_N_BITS"] = struct.unpack("<H", page_header_bytes[42:44])[0]
    # 其余字节可以忽略,因为它们可能未被使用或用于其他目的

    return page_header

def parse_file_trailer(file_trailer_bytes):
    """
    解析 File Trailer
    """
    file_trailer = {}

    file_trailer["CHECKSUM"] = struct.unpack("<I", file_trailer_bytes[0:4])[0]
    file_trailer["LSN"] = struct.unpack("<I", file_trailer_bytes[4:8])[0]

    return file_trailer

# 示例用法
# 假设有 Page Header 和 File Trailer 的字节数据如下(需要根据实际情况修改)
page_header_bytes = bytes.fromhex("00000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000") # 56 字节示例数据
file_trailer_bytes = bytes.fromhex("1234567887654321") # 8 字节示例数据

parsed_page_header = parse_page_header(page_header_bytes)
parsed_file_trailer = parse_file_trailer(file_trailer_bytes)

print(f"Parsed Page Header: {parsed_page_header}")
print(f"Parsed File Trailer: {parsed_file_trailer}")

6. 总结

InnoDB Page 结构复杂而精巧,通过对行记录、Page Header 和 File Trailer 的字节级解析,我们可以更深入地理解 InnoDB 的数据存储和完整性保障机制。掌握这些细节,能够帮助我们更好地优化数据库性能,解决数据损坏问题。理解Page结构是深入理解InnoDB存储引擎的基础。

发表回复

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