首页
  • 监控

    • grafana
    • prometheus
  • 学习笔记

    • 《核心系统命令实战》
    • 《MySQL 是怎样运行的:从根儿上理解 MySQL》
    • 《Ansible权威指南》
  • 博客搭建
  • git
  • python
  • 友情链接
  • 文档编写规范
  • 我用过的电脑
  • 喷涂相关
  • 每日一溜
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

小刘说

砥砺前行
首页
  • 监控

    • grafana
    • prometheus
  • 学习笔记

    • 《核心系统命令实战》
    • 《MySQL 是怎样运行的:从根儿上理解 MySQL》
    • 《Ansible权威指南》
  • 博客搭建
  • git
  • python
  • 友情链接
  • 文档编写规范
  • 我用过的电脑
  • 喷涂相关
  • 每日一溜
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 初识MySQL
  • 启动选项和系统变量
  • 字符集和比较规则
  • InnoDB记录存储结构-行
  • InnoDB记录存储结构-页
    • 1. InnoDB页简介
    • 2. 不同类型的页简介
    • 3. 数据页结构的快速浏览
    • 4. 记录在页中的存储
      • 4.1. 记录头信息
    • 5. Page Directory(页目录)
    • 6. Page Header(页面头部)
    • 7. File Header(文件头部)
    • 8. File Trailer
  • B+ 树索引
  • MySQL 的数据目录
  • 《MySQL 是怎样运行的:从根儿上理解 MySQL》
小刘
2022-12-19
目录

InnoDB记录存储结构-页

# InnoDB记录存储结构-页

# 1. InnoDB页简介

InnoDB需要把内存中的内容刷新到磁盘上采取的方式是:

将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位。InnoDB中页的大小一般为 16 KB。

# 2. 不同类型的页简介

InnoDB为了不同的目的而设计了许多种不同类型的页,比如存放表空间头部信息的页,存放Insert Buffer信息的页,存放INODE信息的页,存放undo日志信息的页等等。

存放我们表中记录类型的页,官方称这种存放记录的页为索引(INDEX)页,鉴于我们还没有了解过索引是个什么东西,而这些表中的记录就是我们日常口中所称的数据,所以目前还是叫这种存放记录的页为数据页吧。

# 3. 数据页结构的快速浏览

数据页代表的这块16KB大小的存储空间可以被划分为多个部分,不同部分有不同的功能,各个部分如图所示:

从图中可以看出,一个InnoDB数据页的存储空间大致被划分成了7个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。下边我们用表格的方式来大致描述一下这7个部分都存储一些啥内容(快速的瞅一眼就行了,后边会详细唠叨的):

名称 中文名 占用空间大小 简单描述
File Header 文件头部 38字节 页的一些通用信息
Page Header 页面头部 56字节 数据页专有的一些信息
Infimum + Supremum 最小记录和最大记录 26字节 两个虚拟的行记录
User Records 用户记录 不确定 实际存储的行记录内容
Free Space 空闲空间 不确定 页中尚未使用的空间
Page Directory 页面目录 不确定 页中的某些记录的相对位置
File Trailer 文件尾部 8字节 校验页是否完整

# 4. 记录在页中的存储

自己存储的记录会按照我们指定的行格式存储到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。图示如下:

# 4.1. 记录头信息

我们先创建一个表:

CREATE TABLE page_demo(
    c1 INT,
    c2 INT,
    c3 VARCHAR(10000),
    PRIMARY KEY (c1)
) CHARSET=ascii ROW_FORMAT=Compact;
1
2
3
4
5
6

注意:把 c1 列指定为主键时,InnoDB不会为我们去创建那个所谓的 row_id 隐藏列。而且我们为这个表指定了ascii字符集以及Compact的行格式。所以这个表中记录的行格式示意图就是这样的:

从图中可以看到,我们特意把记录头信息的5个字节的数据给标出来了,说明它很重要,我们再次先把这些记录头信息中各个属性的大体意思浏览一下(我们目前使用Compact行格式进行演示):

名称 大小(单位:bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录
next_record 16 表示下一条记录的相对位置

由于我们现在主要在唠叨记录头信息的作用,所以为了大家理解上的方便,我们只在page_demo表的行格式演示图中画出有关的头信息属性以及c1、c2、c3列的信息(其他信息没画不代表它们不存在啊,只是为了理解上的方便在图中省略了~),简化后的行格式示意图就是这样:

下边我们试着向page_demo表中插入几条记录:

INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd');
1

为了方便大家分析这些记录在页的User Records部分中是怎么表示的,我把记录中头信息和实际的列数据都用十进制表示出来了(其实是一堆二进制位),所以这些记录的示意图就是:

看这个图的时候需要注意一下,各条记录在User Records中存储的时候并没有空隙,这里只是为了大家观看方便才把每条记录单独画在一行中。我们对照着这个图来看看记录头信息中的各个属性是啥意思:

  • delete_mask:当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。

  • min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记。

  • n_owned:这个暂时保密,稍后它是主角~

  • heap_no:这个属性表示当前记录在本页中的位置,从图中可以看出来,我们插入的4条记录在本页中的位置分别是:2、3、4、5。

    每个页里会自动增加两个记录,称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录。这两条记录的构造都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的,如图所示:

    由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分,如图所示:

    从图中我们可以看出来,最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。

  • record_type:这个属性表示当前记录的类型。0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。

  • next_record:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比方说第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。但是需要注意的一点是,下一条记录指得是按照主键值由小到大的顺序的下一条记录。

    而且规定 Infimum记录(也就是最小记录) 的下一条记录就本页中主键值最小的用户记录,主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录) 。

    从图中可以看出,记录按照主键从小到大的顺序形成了一个单链表:

    如果我们把第2条记录删除掉:

    DELETE FROM page_demo WHERE c1 = 2;
    
    1

    删掉第2条记录后的示意图就是:

    • 第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1。
    • 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。
    • 第1条记录的next_record指向了第3条记录。
    • 就是最大记录的n_owned值从5变成了4。

    所以,不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。

主键值为2的记录被我们删掉了,但是存储空间却没有回收,如果我们再次把这条记录插入到表中,InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间:

INSERT INTO page_demo VALUES(2, 200, 'bbbb');
1

当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。

# 5. Page Directory(页目录)

我们平常想从一本书中查找某个内容的时候,一般会先看目录,找到需要查找的内容对应的书的页码,然后到对应的页码查看内容。设计InnoDB的大叔们为我们的记录也制作了一个类似的目录,他们的制作过程是这样的:

  1. 将所有正常的记录(包括最大和最小记录)划分为组。

  2. 每组的最后一条记录(也就是组内最大的那条记录)的n_owned属性表示该组内共有几条记录。

  3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。

比方说现在的page_demo表中正常的记录共有6条,InnoDB会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录:

从这个图中我们需要注意这么几点:

  • 现在页目录部分中有两个槽,也就意味着我们的记录被分成了两个组,槽0中的值是112,代表最大记录的地址偏移量(就是从页面的0字节开始数,数112个字节);槽1中的值是99,代表最小记录的地址偏移量。

  • 注意最小和最大记录的头信息中的n_owned属性

    • 最小记录的n_owned值为1,这就代表着以最小记录结尾的这个分组中只有1条记录,也就是最小记录本身。
    • 最大记录的n_owned值为5,这就代表着以最大记录结尾的这个分组中只有5条记录,包括最大记录本身还有我们自己插入的4条记录。

为什么最小记录的n_owned值为1,而最大记录的n_owned值为5呢?

每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。

由于现在page_demo表中的记录太少,无法演示添加了页目录之后加快查找速度的过程,所以再往page_demo表中添加一些记录:

INSERT INTO page_demo VALUES(5, 500, 'eeee'), (6, 600, 'ffff'), (7, 700, 'gggg'), (8, 800, 'hhhh'), (9, 900, 'iiii'), (10, 1000, 'jjjj'), (11, 1100, 'kkkk'), (12, 1200, 'llll'), (13, 1300, 'mmmm'), (14, 1400, 'nnnn'), (15, 1500, 'oooo'), (16, 1600, 'pppp');
1

这些记录被分成了5个组,如图所示:

所以在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽。

  2. 通过记录的next_record属性遍历该槽所在的组中的各个记录。

# 6. Page Header(页面头部)

Page Header是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息:

名称 占用空间大小 描述
PAGE_N_DIR_SLOTS 2字节 在页目录中的槽数量
PAGE_HEAP_TOP 2字节 还未使用的空间最小地址,也就是说从该地址之后就是Free Space
PAGE_N_HEAP 2字节 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)
PAGE_FREE 2字节 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用)
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_LEVEL 2字节 当前页在B+树中所处的层级
PAGE_INDEX_ID 8字节 索引ID,表示当前页属于哪个索引
PAGE_BTR_SEG_LEAF 10字节 B+树叶子段的头部信息,仅在B+树的Root页定义
PAGE_BTR_SEG_TOP 10字节 B+树非叶子段的头部信息,仅在B+树的Root页定义

如果大家认真看过前边的文章,从PAGE_N_DIR_SLOTS到PAGE_LAST_INSERT以及PAGE_N_RECS的意思大家一定是清楚的,如果不清楚,对不起,你应该回头再看一遍前边的文章。剩下的状态信息看不明白不要着急,饭要一口一口吃,东西要一点一点学(一定要稍安勿躁哦,不要被这些名词吓到)。在这里我们先唠叨一下PAGE_DIRECTION和PAGE_N_DIRECTION的意思:

  • PAGE_DIRECTION:最后一条记录插入方向。如果新插入的一条记录的主键值比上一条记录的主键值比上一条记录大,我们说这条记录的插入方向是右边,反之则是左边。

  • PAGE_N_DIRECTION:假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。

# 7. File Header(文件头部)

File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,占用固定的38个字节,是由下边这些内容组成的:

名称 占用空间大小 描述
FIL_PAGE_SPACE_OR_CHKSUM 4字节 页的校验和(checksum值)
FIL_PAGE_OFFSET 4字节 页号
FIL_PAGE_PREV 4字节 上一个页的页号
FIL_PAGE_NEXT 4字节 下一个页的页号
FIL_PAGE_LSN 8字节 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number)
FIL_PAGE_TYPE 2字节 该页的类型
FIL_PAGE_FILE_FLUSH_LSN 8字节 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4字节 页属于哪个表空间
  • FIL_PAGE_SPACE_OR_CHKSUM:当前页面的校验和(checksum)。

  • FIL_PAGE_OFFSET:页号。

  • FIL_PAGE_TYPE:当前页的类型,其他类型的页,具体如下表:

    类型名称 十六进制 描述
    FIL_PAGE_TYPE_ALLOCATED 0x0000 最新分配,还没使用
    FIL_PAGE_UNDO_LOG 0x0002 Undo日志页
    FIL_PAGE_INODE 0x0003 段信息节点
    FIL_PAGE_IBUF_FREE_LIST 0x0004 Insert Buffer空闲列表
    FIL_PAGE_IBUF_BITMAP 0x0005 Insert Buffer位图
    FIL_PAGE_TYPE_SYS 0x0006 系统页
    FIL_PAGE_TYPE_TRX_SYS 0x0007 事务系统数据
    FIL_PAGE_TYPE_FSP_HDR 0x0008 表空间头部信息
    FIL_PAGE_TYPE_XDES 0x0009 扩展描述页
    FIL_PAGE_TYPE_BLOB 0x000A BLOB页
    FIL_PAGE_INDEX 0x45BF 索引页,也就是我们所说的数据页

    我们存放记录的数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页。

  • FIL_PAGE_PREV:本页的上一个页号。

  • FIL_PAGE_NEXT:本页的下一个页号。

# 8. File Trailer

每个页的尾部都有一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:

  • 页的校验和(前4个字节):当页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。

  • 页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的。

这个File Trailer与FILE Header类似,都是所有类型的页通用的。

上次更新: 2024/05/11, 03:55:33

← InnoDB记录存储结构-行 B+ 树索引→

最近更新
01
kubernetes控制器-Service
08-18
02
kubernetes控制器-Deployment
08-08
03
kubernetes调度基础
07-27
更多文章>
Theme by Vdoing | Copyright © 2023-2024 本站支持IPv6访问 本站支持SSL安全访问
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式