- 从前面介绍我们已经知道页是InnoDB存储引擎管理数据库的最小磁盘单位
- 页类型为B-tree Node的页(数据页)存放的即是表中行的实际数据了
- InnoDB数据页由以下7个部分组成:
- File Header(文件头)
- Page Header(页头)
- Infimun和Supermum Records
- User Records(用户记录,即行记录)
- Free Space(空闲目录)
- Page Directory(页目录)
- File Trailer(文件结尾信息)
- 其中File Header、Page Header、File Trailer的大小是固定的,分别为38、56、8字节,这些空间用来标记该页的一些信息,如Checksum,数据页所在B+树索引的层数等
- User Records、Free Space、Page Directory这些部分为实际的行记录存储空间,因此大小是动态的
一、File Header
- File Header用来记录页的一些头信息。由下表中8个部分组成,供占用38字节
二、Page Header
- File Header的后面一部分为Page Header,该部分用来记录数据页的状态信息,由下面14个部分组成,共占用56字节
三、Infimum和Supermum Record
- 在InnoDB中,每个数据页中有两个虚拟的行记录,用来限定记录的边界
- Infumum记录是比该页中任何主键值都要小的值。Supermum指比任何可能打的值还要大的值
- 这两个值在页创建时被建立,并且在任何情况下不会被删除
- 在Compact行格式和Redundant行格式下,两者占用的字节数各不相同
- 下图显示了Infumum和Supermum记录:
四、User Record和Free Space
User Record
- User Record就是之前讨论过的部分,即实际存储行记录的内容
- 再次强调,InnoDB存储引擎表总是B+树索引组织的
Freee Space
- 指的是页的空闲空间,同样也是个链表数据结构
- 在一条记录被删除后,删除的空间会被加入到空闲链表中
五、Page Directory
- Page Directory(页目录)中存放了记录的相对位置(注意,这里存放的是页相对位置,而不是偏移量)。有些时候这些记录指针称为Slots(槽)或目录槽(Directory Slots)
- 与其他数据库系统不同的是,在InnoDB中并不是每个记录拥有一个槽,InnoDB的槽是一个稀疏目录,即一个槽中可能包含多个记录。伪记录Infimum的n_owned值总是1,记录Supermum的n_owned的取值范围为[1,8],其他用户记录n_owned的取值范围为[4,8]。当记录被插入或删除时需要对槽进行分裂或平衡的维护操作
- 在Slots中记录按照索引键值顺序存放,这样可以利用二叉查找迅速找到记录的指针。假设有('i','d','c','b','e','g','l','h','f','j','k','a'),同时假设一个槽中包含4条记录,则Slots中的记录可能是('a','e','i')
- 由于在InnoDB中Page Directory是稀疏目录,二叉查找的结果只是一个粗略的结果,因此InnoDB存储引擎必须通过recorder header中的next_record来继续查找相关记录。同时,Page Directory很好地解释了recorder header中的n_owned值的含义,因为这些记录并不包括在Page Directory中
- 需要牢记的是,B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过Page Directory再进行二叉查找。只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间
- recorder header和n_owned参阅:https://blog.csdn.net/qq_41453285/article/details/104125757
六、File Trailer
- 为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等),InnoDB存储引擎的页中设置了File Trailer部分
工作原理
- File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节
- 前4字节代表该页的checksum值
- 最后4字节和File Header中的FIL_PAGE_LSN相同
- 将这两个值与File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值进行比较,看是否一致(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性
innodb_checksums参数
- 在默认配置下,InnoDN存储引擎每次从磁盘读取一个页就会检测该页的完整性,即页是否发生Corrupt,这就是通过File Trailer部分进行检测,而该部分的检测会有一定的开销
- 用户可以通过该参数来开启或关闭对这个页完整性的检查
innodb_checksum_algorithm参数
- MySQL 5.6.6版本增加了这个参数,该参数用来控制检测checksum函数的算法
- 默认值为crc32,可设置的值有:innodb、crc32、none、strict_innodb、strict_crc32、strict_none
- innodb为兼容之前版本InnoDB页的checksum检测方式,crc32为MySQL 5.6.6版本引进的新的checksum算法,该算法较之前的innodb有着较高的性能
- 但是若表中所有页的checksum值都以strict算法保存,那么低版本的MySQL数据库将不能读取这些页
- none表示不对页启用checksum检查
- strict_*正如其名,表示严格地按照设置的checksum算法进行页的检测。因此若低版本MySQL数据库升级到MySQL 5.6.6或之后的版本,启用strict_crc32将导致不能读取表中的页。启用strict_crc32方式是最快的方式,因为其不再对innodb和crc32算法进行两次检测。故推荐使用该设置。若数据库从低版本升级而来,则需要进行mysql_upgrade操作
七、InnoDB数据页结构示例分析
第一步:
drop table if exists t;
create table t(
a int unsigned not null auto_increment,
b char(10),
primary key(a)
);ENGINE=InnoDB CHARSET=UTF8;
delimiter $$
create procedure load_t(count int unsigned)
begin
set @c=0;
while @c<count
do
select NULL,repeat(char(97+RAND()*26),10);
set @c=@c+1;
end while;
end;
$$
delimiter ;
call load_t(100);
select a,b from t limit 10;
第二步:
- 用工具py_innodb_page_info分析t.ibd文件,结果如下:
第三步:
- 可以发现第4个页(page offset 3)是数据页,然后通过hexdump来分析t.ibd文件,打开整理得到的十六进制文件,数据页从0x0000c000(16K*3=0xc000)处开始,得到以下内容:
- 52 1b 24 00:数据页的checksum值
- 00 00 00 03:页的偏移量,从0开始
- ff ff ff ff:前一个页,因为只有当前一个数据页,所以这里为0xffffffff
- ff ff ff ff:下一个页,因为只有当前一个数据页,所以这里为0xffffffff
- 00 00 00 0a 6a e0 ac 93:页的LSN
- 45 bf:页类型,0x45bf代表数据页
- 00 00 00 00 00 00 00 :这里暂时不管
- 00 00 00 dc:表空间的SPACE ID
第四步:
- 先不看Page Header,先看File Trailer部分。因为File Trailer通过比较File Header部分来保证页的写入的完整性。File Trailer的8字节为:
- 95 ae 5d 39:checksum值,该值通过checksum函数与File Header部分的checksum值进行比较
- 6a e0 ac 93:注意该值和File Header部分页的LSN后4个值相等
第五步:
- 接着分析56字节的Page Header部分。对于数据页而言,Page Header部分保存了该页中行记录的大量细节信息。分析后得:
- PAGE_N_HEAP=0x8066:当行记录格式为Compact时,初始值为0x0802;当行格式为Redundant时,初始值为2.其实这些值表示页初始化时就已经有Infinmum和Supremum的伪记录行,0x8066-0x8002=0x64,代表该页中实际的记录有100条记录
- PAGE_FREE=0x0000:代表可重用的空间首地址,因为这里没有进行过任何删除操作,故这里的值为0
- PAGE_GRABAGE=0x0000:代表删除的记录字节为0,同样因为我们没有进行过删除操作,这里的值依然为0
- PAGE_DIRECTION=0x0002:因为通过自增长的方式进行行记录的插入,所以PAGE_DIRECTION的方向是向右,为0x00002
- PAGE_N_DIRECTION=0x0063:表示一个方式连续插入记录的数量,因为我们是自增长的方式插入了100条记录,因此该值为99
- PAGE_N_RECS=0x0064:表示该页中的行记录树为100,注意该值与PAGE_N_HEAP的比较,PAGE_N_HEAP包含两个伪行记录,并且是通过有符号的方式记录的,因此值为0x8066
- PAGE_LEVEL=0x00:代表该叶子节点。因为数据量目前比较晒,因此当前B+树索引只有一层。B+树叶子层总是0x00
- PAGE_INDEX_ID=0x00000000000001ba:索引ID
- PAGE_N_DIR_SLOTS=0x001a,代表Page Directory有26个槽,每个槽占用2字节,我们可以从0x0000ffc4到0x0000fff7中找到如下内容:
- PAGE_HEAP_TOP=0x0dc0,代表空闲空间开始位置的偏移量,即0xc000+0x0dc0=0xcdc0处开始,观察这个位置的情况,可以发现这的确是最后一行的结束,接下去的部分都是空闲空间了
- PAGE_LAST_INSERT=0x0da5,表示页最后插入的位置的偏移量,即最后的插入位置应该在0xc0000+0x0da5=0xcda5,查看该位置如下图所示,可以看到的确最后插入a列值为100的行记录,但是这次直接指向了行记录的内容,而不是指向行记录的变长字段长度的列表位置:
第六步:
- 上面就是数据页的Page Header部分了,接下来就是存放的行记录了,前面提到过InnoDB存储引擎有两个伪记录,用来限定行记录的边界,接着往下看:
- 观察0xc05E到0xc007,这里存放的就是这两个伪行记录,在InnoDB存储引擎中设置伪行只有一个列,且类型是char(8)。伪行记录的读取方式和一般的行记录并无不同,我们整理后可以得到如下结果:
- 然后来分析infumum行记录的recorder header部分,最后两个字节为00 1c表示下一个记录的位置的偏移量,即当前行记录内容的位置0xc063+0x001c,即0xc07f。0xc07f应该很熟悉了,之前分析的行记录结构都是从这个位置开始的,如:
- 可以看到这就是第一条实际行记录内容的位置了,整理后得:
- 通过recorder header的最后两个字节记录的下一行记录的偏移量就可以得到该页中所有的行记录,通过Page Header的PAGE_PREV和PAGE_NEXT就可以知道上个页和下个页的位置,这样InnoDB存储引擎就能读到整张表所有的行记录数据
第七步:
- 最后分析Page Directory。前面提到了从0x0000ffc4到0x0000fff7是当前页的Page Directory,如下:
- 需要注意的是,Page Directory是逆序存放的,每个槽占2字节,因此可以看到00 63是最初行的相对位置,即0xc063;00 70就是最后一行记录的相对位置,即0xc070。
- 我们发现这就是前面分析的Infimum和Supremum的伪行记录。Page Directory槽中的数据都是按照主键的顺序存放的,因此查询具体记录就需要通过部分进行。前面已经提到InnoDB存储引擎的槽是稀疏的,故还需要通过Recorder Header的n_owned进行进一步的判断,如InnoDB存储引擎需要找到主键a为5的记录,通过二叉查找Page Directory的槽,可以定位记录的相对位置在00 e5处,执行行记录的实际位置0xc0e5
- 可以看到第一行的记录时4,不是我们要找的6,但是可以发现前面的5字节的Record Header为04 00 28 00 22。找到4~8位表示n_owned值的部分,该值为4,表示该记录有4个记录,因此还需要进一步查找,通过recorder header最后两个字节的偏移量0x0022找到下一条记录的位置为0xc107,这才是最终要找到的主键为5的记录