【InnoDB 存储引擎】二进制详细分析 InnoDB 是如何存储数据的(实验课版)(通过分析数据页的格式读者会对InnoDB底层如何存储数据有非常好的理解,这也是理解索引底层原理的一种方式)

前言:

1、本文是对 InnoDB 行格式的实验,请先通读 InnoDB 行格式的理论后再来实验。理论参考我的文章:《InnoDB 行格式》

2、实验环境是 mysql 的 5.7.41,建议读者也采用 5.7 版本,如果是 8.0 版本会有些不同之处

3、mysql 中一般一页是 16KB 大小,所以有如下的页地址列表

第几页页起始地址
100000000
200004000
300008000
40000c000
500010000
600014000

4、从 0000c000 页开始为数据页(当然也不一定但本实验是这样的)

1 探索数据页格式

1.1 实验步骤

  1. 准备表准备数据

    create table t(
        a int unsigned NOT NULL AUTO_INCREMENT,
        b char(10),
        PRIMARY KEY (a)
    )ENGINE = innodb CHARSET = utf8 row_format=Compact;
    
    -- 创建存储过程插入 100 条数据,然后分析页格式
    DELIMITER $$
    CREATE PROCEDURE load_t (count int unsigned)
    BEGIN 
        set @c = 0;
        WHILE (@c < count) do
            INSERT INTO t SELECT null, repeat(char(97+rand()*26),10);
            set @c = @c + 1;
        END WHILE;
    END $$
    
    call load_t(100);
    select count(*) from t;
    
  2. 用 hexdump 工具把 ibd 文件转换成标准的十六进制形式

    # 把 idb 文件导出为 十六进制的 txt 格式
    hexdump -C -v t.ibd > t.txt
    
  3. 定位到数据页在那一页

    我们只插入了100条数据,数据量有限,只占用了一个数据页,数据页的地址是:0000c000

  4. 把数据页的第一页的数据捞出来分析

    3073 0000c000  ee 02 36 2f 00 00 00 03  ff ff ff ff ff ff ff ff  |..6/............|
    3074 0000c010  00 00 00 00 22 de 97 0d  45 bf 00 00 00 00 00 00  |...."...E.......|
    3075 0000c020  00 00 00 00 00 5b 00 1a  0d c0 80 66 00 00 00 00  |.....[.....f....|
    3076 0000c030  0d a5 00 02 00 63 00 64  00 00 00 00 00 00 00 00  |.....c.d........|
    3077 0000c040  00 00 00 00 00 00 00 00  00 65 00 00 00 5b 00 00  |.........e...[..|
    3078 0000c050  00 02 00 f2 00 00 00 5b  00 00 00 02 00 32 01 00  |.......[.....2..|
    3079 0000c060  02 00 1c 69 6e 66 69 6d  75 6d 00 05 00 0b 00 00  |...infimum......|
    3080 0000c070  73 75 70 72 65 6d 75 6d  0a 00 00 00 10 00 22 00  |supremum......".|
    3081 0000c080  00 00 01 00 00 00 00 2e  0a aa 00 00 01 90 01 10  |................|
    3082 0000c090  68 68 68 68 68 68 68 68  68 68 0a 00 00 00 18 00  |hhhhhhhhhh......|
    3083 0000c0a0  22 00 00 00 02 00 00 00  00 2e 0b ab 00 00 01 92  |"...............|
    3084 0000c0b0  01 10 75 75 75 75 75 75  75 75 75 75 0a 00 00 00  |..uuuuuuuuuu....|
    3085 0000c0c0  20 00 22 00 00 00 03 00  00 00 00 2e 0e ad 00 00  | .".............|
    3086 0000c0d0  01 a0 01 10 65 65 65 65  65 65 65 65 65 65 0a 00  |....eeeeeeeeee..|
    3087 0000c0e0  04 00 28 00 22 00 00 00  04 00 00 00 00 2e 0f ae  |..(."...........|
    3088 0000c0f0  00 00 01 93 01 10 6d 6d  6d 6d 6d 6d 6d 6d 6d 6d  |......mmmmmmmmmm|
    3089 0000c100  0a 00 00 00 30 00 22 00  00 00 05 00 00 00 00 2e  |....0.".........|
    3090 0000c110  12 b0 00 00 01 1a 01 10  78 78 78 78 78 78 78 78  |........xxxxxxxx|
    3091 0000c120  78 78 0a 00 00 00 38 00  22 00 00 00 06 00 00 00  |xx....8.".......|
    

1.2 分析捞出来的数据

1.2.1 File Header 的 38 字节

File Header用来记录页的一些头信息,由下表中的8个部分组成,共占用38字节。

名称大小(字节)说明
FIL_PAGE_SPACE_OR_CHKSUM4该值代表页的checksum值
FIL_PAGE_OFFSET4表空间中页的偏移位。如某独立表空间a.ibd的大小为1GB,如果页的大小为16KB,那么总共有65536个页。FIL_PAGE_OFFSET表示该页在所有页中的位置。若此表空间的ID为10,那么搜索页(10,1)就表示查找表a中的第二个页
FIL_PAGE_PREV4当前页的上一个页,B+ Tree特性决定了叶子节点必须是双向列表
FIL_PAGE_NEXT4当前页的下一个页,B+Tree特性决定了叶子节点必须是双向列表
FIL_PAGE_LSN8该值代表该页最后被修改的日志序列位置LSN (Log Sequence Number)
FIL_PAGE_TYPE2InnoDB存储引擎页的类型,记住0x45BF,该值代表了存放的是数据页,即实际行记录的存储空间
FIL_PAGE_FILE_FLUSH_LSN8该值仅在系统表空间的一个页中定义
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID4该值代表页属于哪个表空间
// 数据的范围用[]框出
3073 0000c000  [ee 02 36 2f 00 00 00 03  ff ff ff ff ff ff ff ff  |..6/............|
3074 0000c010  00 00 00 00 22 de 97 0d  45 bf 00 00 00 00 00 00  |...."...E.......|
3075 0000c020  00 00 00 00 00 5b] 00 1a  0d c0 80 66 00 00 00 00  |.....[.....f....|
  • FIL_PAGE_SPACE_OR_CHKSUM

    ee 02 36 2f

  • FIL_PAGE_OFFSE

    00 00 00 03

    表明是表空间的第四页,确实是第四页,没问题

  • FIL_PAGE_PREV

    ff ff ff ff

    上一页的地址,ffffffff说明没有上一页,即这是第一页

  • FIL_PAGE_NEXT

    ff ff ff ff

    下一页的地址,ffffffff说明没有下一页,即这是最后一页

  • FIL_PAGE_LSN

    00 00 00 00 22 de 97 0d

  • FIL_PAGE_TYPE

    45 bf

  • FIL_PAGE_FILE_FLUSH_LSN

    00 00 00 00 00 00 00 00

  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

    00 00 00 5b

1.2.2 Page Header 的 56 个字节

接着 File Header 部分的是 Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节,如下表所示

名称大小(字节)说明
PAGE_N_DIR_SLOTS2在 Page Directory (页目录)中的 Slot (槽)数。后文重点介绍
PAGE_HEAP_TOP2堆中第一个记录的指针,记录在页中是根据堆的形式存放的
PAGE_N_HEAP2堆中的记录数。一共占用2字节,但是第15位指示行记录格式
PAGE_FREE2指向可重用空间的首指针
PAGE_GARBAGE2已删除记录的字节数,即行记录结构中delete flag为1的记录大小的总数
PAGE_LAST_INSERT2最后插入记录的位置
PAGE_DJRECTION2最后插入的方向。
PAGE_N_DIRECTION2一个方向连续插入记录的数量
PAGE_N_RECS2该页中记录的数量
PAGE_MAX_TRX_ID8修改当前页的最大事务ID,注意该值仅在Secondary Index中定义
PAGE_LEVEL2当前页在索引树中的位置,0x00代表叶节点,即叶节点总是在第0层
PAGE_INDEX_ID8索引ID,表示当前页属于哪个索引
PAGE_BTR_SEG_LEAF10B+树数据页非叶节点所在段的segment header。注意该值仅在B+树的Root页中定义
PAGE_BTR_SEG_TOP10B+树数据页所在段的segment header。注意该值仅在B+树的Root页中定义
// 范围用[]框出
3075 0000c020  00 00 00 00 00 5b [00 1a  0d c0 80 66 00 00 00 00  |.....[.....f....|
3076 0000c030  0d a5 00 02 00 63 00 64  00 00 00 00 00 00 00 00  |.....c.d........|
3077 0000c040  00 00 00 00 00 00 00 00  00 65 00 00 00 5b 00 00  |.........e...[..|
3078 0000c050  00 02 00 f2 00 00 00 5b  00 00 00 02 00 32] 01 00  |.......[.....2..|

Page Header (56 bytes):

  • PAGE_N_DIR_SLOTS = 00 1a

    00 1a 为 26,即 26 个槽,每个槽占用 2 个字节,那么一共占用了 52 个字节。详细的数据请参考 Page Directory

  • PAGE_HEAP_TOP = 0d c0

    代表空闲空间开始位置的偏移量。具体位置即 c000 + 0dc0 = cdc0,查看 cdc0 的数据

    3292 0000cdb0  00 00 01 c2 01 10 77 77  77 77 77 77 77 77 77 77  |......wwwwwwwwww|
    3293 0000cdc0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
    
    -- 最后一条记录
    mysql> select * from t where a = 100;
    +-----+------------+
    | a   | b          |
    +-----+------------+
    | 100 | wwwwwwwwww |
    +-----+------------+
    1 row in set (0.00 sec)
    

    最后一条记录确实是 wwwwwwwwww,果然它后面的空间都是空闲的,都是 0000000

  • PAGE_N_HEAP = 80 66

    80 66 中的 80 表示有符号,66 表示 102。为什么是 102 因为包含了 2 条伪记录 + 100 条真是记录

  • PAGE_FREE = 00 00

    可重用的空间首地址,因为第一页被占满了,所以是 0

  • PAGE_GARBAGE = 00 00

    代表删除的记录字节为 0,因为没有删除,所以是 0

  • PAGE_LAST_INSERT = 0d a5

    最后插入记录的位置,c000 + 0da5 = cda5,查看该位置的数据果然是主键 id 最大的位置

    // 范围用[]框出
    3291 0000cda0  00 03 28 f2 cb [00 00 00  64 00 00 00 00 2e 71 ef  |..(.....d.....q.|
    3292 0000cdb0  00 00 01 c2 01 10 77 77  77 77 77 77 77 77 77 77]  |......wwwwwwwwww|
    
    -- 最后一条记录
    mysql> select * from t where a = 100;
    +-----+------------+
    | a   | b          |
    +-----+------------+
    | 100 | wwwwwwwwww |
    +-----+------------+
    1 row in set (0.00 sec)
    

    cda5 位置即00 00 00 64,因为是无符号的 int 自增主键,所以占用 4 个字节,换算成 10 进制即 a = 100

  • PAGE_DJRECTION = 00 02

    解释:插入方向向右

  • PAGE_N_DIRECTION = 00 63

    一个方向的连续插入记录,十六进制 00 63 换算为十进制为 99 条,一共 100 条数据,连续一个方向插入了 99 条

  • PAGE_N_RECS = 00 64

    该页中记录的数量,00 64 表示 100 条,我们刚好一共也就插入了 100 条数据

  • PAGE_MAX_TRX_ID = 00 00 00 00 00 00 00 00

  • PAGE_LEVEL = 00 00

  • PAGE_INDEX_ID = 00 00 00 00 00 00 00 65

  • PAGE_BTR_SEG_LEAF = 00 00 00 5b 00 00 00 02 00 f2

  • PAGE_BTR_SEG_TOP = 00 00 00 5b 00 00 00 02 00 32

1.2.3 Infimum 和 Supremum Record

在InnoDB存储引擎中, 每个数据页中有两个虚拟的行记录, 用来限定记录的边界。在 Page Header 之后就是它了,那么怎么知道它的数据范围呢?

其实可以直接搜索英文关键词:infimum 和 supremum,再者伪列的结构是:[ record header + char(8)],就知道了。

而理论上:

1、根据 Page Directory第一个槽 00 63 可以知道 Infimum 的地址为:c063

2、c063 表示的是 infimum 字符的地址,再结合伪列的结构:前5个字节的 record header 和 伪列的列类型 char(8)

伪记录跟普通记录类似但是也有区别,区别在于它只有 record header 5 个字节和一个列,类型是 char(8),而不像其他普通记录那样有标识列长度的字节、null 列表字节、rowid 字节、事务 ID字节等

下面贴出它的数据:

// 范围用[]框出
3078 0000c050  00 02 00 f2 00 00 00 5b  00 00 00 02 00 32 [01 00  |.......[.....2..|
3079 0000c060  02 00 1c 69 6e 66 69 6d  75 6d 00 05 00 0b 00 00  |...infimum......|
3080 0000c070  73 75 70 72 65 6d 75 6d]  0a 00 00 00 10 00 22 00  |supremum......".|
  • Infimum 伪行记录

    record hader:01 00 02 00 1c

    列数据:69 6e 66 69 6d 75 6d 00(代表字符 infimum 多了一个 00 )

    说明:

    第一条记录的位置为:Infimum + 偏移量,即 c063 + 001c = c07f,查看 c07f 的数据如下:

    // 范围用[]框出
    3080 0000c070  73 75 70 72 65 6d 75 6d  0a 00 00 00 10 00 22 [00  |supremum......".|
    3081 0000c080  00 00 01 00 00 00 00 2e  0a aa 00 00 01 90 01 10  |................|
    3082 0000c090  68 68 68 68 68 68 68 68  68 68] 0a 00 00 00 18 00  |hhhhhhhhhh......|
    
    mysql> select * from t where a = 1;
    +---+------------+
    | a | b          |
    +---+------------+
    | 1 | hhhhhhhhhh |
    +---+------------+
    1 row in set (0.00 sec)
    

    主键是 00 00 00 01,正好是第一条记录即 id=1

  • Supremum 伪行记录

    record hader:05 00 0b 00 00

    列数据:3 75 70 72 65 6d 75 6d(代表字符 supremum )

1.2.4 Page Directory(重点)

Page Header 中前2个字节PAGE_N_DIR_SLOTS指示了Page Directory有多少个槽,从[00 1a]可以知道有26个槽

Page Directory 在 File Tailer 前面,File Tailer 固定占用8个字节,而 Page Directory 有 26个槽,每个槽占用2个字节

定位槽的数据:

// 数据用[]框出
4093 0000ffc0  00 00 00 00 [00 70 0d 1d  0c 95 0c 0d 0b 85 0a fd  |.....p..........|
4094 0000ffd0  0a 75 09 ed 09 65 08 dd  08 55 07 cd 07 45 06 bd  |.u...e...U...E..|
4095 0000ffe0  06 35 05 ad 05 25 04 9d  04 15 03 8d 03 05 02 7d  |.5...%.........}|
4096 0000fff0  01 f5 01 6d 00 e5 00 63]  ee 02 36 2f 22 de 97 0d  |...m...c..6/"...|

槽的规则:

因为槽是倒序的,一个槽占用2个字节,一个槽可以对应多个记录行。上面的表格中一共有 26 个槽,0063 是指向伪记录的 Infimum,0070 指向伪记录的 Supremum,那么还剩下 24 个槽。下面逐个分析 24 个槽,槽是相对位置要加上页的偏移量

00e5 -> c0e5 -> 00 00 00 04(即 id=4)

016d -> c16d -> 00 00 00 08(即 id=8)

01f5 -> c1f5 -> 00 00 00 0c(即 id=12)

027d -> c27d -> 00 00 00 10(即 id=16)

0d1d -> cd1d -> 00 00 00 60(即 id=96)

大致是4条记录占用一个槽,24 * 4 = 96,一共100条记录,差不多也是平均分配槽位

如何查找某一条记录(重点):

以查找 id 为 5 的记录举例:

  1. 先找到具体是哪一页

    先按一定的方法找到是 c000 页

  2. 加载页到内存中

  3. 在内存中用二分法找到具体是哪一个槽

    通过二分法定位到 id=5 的记录在第2个槽,即 00 e5 槽中(4 <= id < 8)

  4. 然后找具体记录

    找到 id=4 的记录后,遍历链表找到 id=5 的记录

查找一直都是直接找到 rowId(主键) 的位置,而没有 rowId 前面的 record header 等

1.2.5 File Tailer 的 8 个字节

因为 File Tailer 在页的尾部位置,也就是在下一页之前的 8 个字节。
下一页的位置是:当前页位置 0xC000 + 16KB = 0xG000,那么 File Tailer 的位置就是 0xFFF0

// 数据范围用[]框出
4096 0000fff0  01 f5 01 6d 00 e5 00 63  [ee 02 36 2f 22 de 97 0d]  |...m...c..6/"...|

ee 02 36 2f 22 de 97 0d

ee 02 36 2f:用来与 File Header 中的 FIL_PAGE_SPACE_OR_CHKSUM 比较

22 de 97 0d:用来与 File Hader 中的 FIL_PAGE_LSN 比较

1、File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节。

2、前4字节代表该页的checksum值,最后4字节和File Header中的FIL PAGE LSN相同。

3、将这两个值与 File Header 中的 FIL_PAGE_SPACE_OR_CHKSUM 和 FIL_PAGE_LSN 值进行比较,看是否一致(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较), 以此来保证页的完整性

2 参考资料

数据页的格式:我的文章:《15.6.1 InnoDB 数据页格式(数据页理论,重要).md》

书籍:《InnoDB 存储引擎》,该书电子版书籍作者无套路免费下载


传送门: 保姆式Spring5源码解析

欢迎与作者一起交流技术和工作生活

联系作者

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fire Fish

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值