前言:
1、本文是对 InnoDB 行格式的实验,请先通读 InnoDB 行格式的理论后再来实验。理论参考我的文章:《InnoDB 行格式》
2、实验环境是 mysql 的
5.7.41
,建议读者也采用 5.7 版本,如果是 8.0 版本会有些不同之处3、mysql 中一般一页是 16KB 大小,所以有如下的页地址列表
第几页 页起始地址 1 00000000 2 00004000 3 00008000 4 0000c000 5 00010000 6 00014000 4、从
0000c000
页开始为数据页(当然也不一定但本实验是这样的)
准备表准备数据
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;
用 hexdump 工具把 ibd 文件转换成标准的十六进制形式
# 把 idb 文件导出为 十六进制的 txt 格式
hexdump -C -v t.ibd > t.txt
定位到数据页在那一页
我们只插入了100条数据,数据量有限,只占用了一个数据页,数据页的地址是:0000c000
把数据页的第一页的数据捞出来分析
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.".......|
File Header用来记录页的一些头信息,由下表中的8个部分组成,共占用38字节。
名称 | 大小(字节) | 说明 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 该值代表页的checksum值 |
FIL_PAGE_OFFSET | 4 | 表空间中页的偏移位。如某独立表空间a.ibd的大小为1GB,如果页的大小为16KB,那么总共有65536个页。FIL_PAGE_OFFSET表示该页在所有页中的位置。若此表空间的ID为10,那么搜索页(10,1)就表示查找表a中的第二个页 |
FIL_PAGE_PREV | 4 | 当前页的上一个页,B+ Tree特性决定了叶子节点必须是双向列表 |
FIL_PAGE_NEXT | 4 | 当前页的下一个页,B+Tree特性决定了叶子节点必须是双向列表 |
FIL_PAGE_LSN | 8 | 该值代表该页最后被修改的日志序列位置LSN (Log Sequence Number) |
FIL_PAGE_TYPE | 2 | InnoDB存储引擎页的类型,记住0x45BF,该值代表了存放的是数据页,即实际行记录的存储空间 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 该值仅在系统表空间的一个页中定义 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 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....|
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
接着 File Header 部分的是 Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节,如下表所示
名称 | 大小(字节) | 说明 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在 Page Directory (页目录)中的 Slot (槽)数。后文重点介绍 |
PAGE_HEAP_TOP | 2 | 堆中第一个记录的指针,记录在页中是根据堆的形式存放的 |
PAGE_N_HEAP | 2 | 堆中的记录数。一共占用2字节,但是第15位指示行记录格式 |
PAGE_FREE | 2 | 指向可重用空间的首指针 |
PAGE_GARBAGE | 2 | 已删除记录的字节数,即行记录结构中delete flag为1的记录大小的总数 |
PAGE_LAST_INSERT | 2 | 最后插入记录的位置 |
PAGE_DJRECTION | 2 | 最后插入的方向。 |
PAGE_N_DIRECTION | 2 | 一个方向连续插入记录的数量 |
PAGE_N_RECS | 2 | 该页中记录的数量 |
PAGE_MAX_TRX_ID | 8 | 修改当前页的最大事务ID,注意该值仅在Secondary Index中定义 |
PAGE_LEVEL | 2 | 当前页在索引树中的位置,0x00代表叶节点,即叶节点总是在第0层 |
PAGE_INDEX_ID | 8 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10 | B+树数据页非叶节点所在段的segment header。注意该值仅在B+树的Root页中定义 |
PAGE_BTR_SEG_TOP | 10 | B+树数据页所在段的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
在InnoDB存储引擎中, 每个数据页中有两个虚拟的行记录, 用来限定记录的边界。在 Page Header 之后就是它了,那么怎么知道它的数据范围呢?
其实可以直接搜索英文关键词:infimum 和 supremum,再者伪列的结构是:[ record header + char(8)],就知道了。
而理论上:
1、根据
Page Directory
第一个槽 00 63 可以知道 Infimum 的地址为:c0632、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 )
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 的记录举例:
先找到具体是哪一页
先按一定的方法找到是 c000 页
加载页到内存中
在内存中用二分法找到具体是哪一个槽
通过二分法定位到 id=5 的记录在第2个槽,即 00 e5 槽中(4 <= id < 8)
然后找具体记录
找到 id=4 的记录后,遍历链表找到 id=5 的记录
查找一直都是直接找到 rowId(主键) 的位置,而没有 rowId 前面的 record header 等
因为 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函数来进行比较,不是简单的等值比较), 以此来保证页的完整性
数据页的格式:我的文章:《15.6.1 InnoDB 数据页格式(数据页理论,重要).md》
书籍:《InnoDB 存储引擎》,该书电子版书籍作者无套路免费下载
传送门: 保姆式Spring5源码解析
欢迎与作者一起交流技术和工作生活
联系作者