MySQL基础 - Innodb数据页
InnoDB数据存储模型用"spaces"表示,也被称为"tablespaces",有时候也被称为"file spaces",一个space可能包含多个操作系统级别的实际文件(例如ibdata1,ibdata2等)。但是只是被当做一个逻辑文件:多个物理文件只是被视为物理连接在一起。
InnoDB中每个space都会被分配一个32位整型的space ID,被许多其他地方用来关联这个space的。InnoDB总是有一个 “system space”,它的space ID总是为0,system space用于InnoDB需要的各种特殊簿记。
每个MySQL表创建一个.idb文件。但是从内部来看,.idb文件是一个可以包含多个表的space,只不过MySQL的实现,每个.ibd文件只包含一个表。
Pages
- 1.页是innodb管理存储空间的基本单位
- 2.一般大小是16kb
- 3.不同的页存储不同的数据类型,比如存放表空间头部信息的页,存放insertBuffer信息的页面,存放INODE的页,存放undo日志信息或者索引页(数据页)
每个space被分割为多个page,通常每个page是16k(变量innodb_page_size
,编译mysql时可以通过修改UNIV_PAGE_SIZE来改变page的大小)。
space中的每个page被分配了一个32位整型的页号(page number),被称为offset
,这个offset实际上就是page相对space起始位置的偏移量。所以,page 0就是偏移量offset=0的文件,page 1就是偏移量offset=16384的文件,以此类推。
我们都知道InnoDB数据上限是64TB,这个限制实际上是每个space的限制。主要是因为页号是32位整型,并且默认pageSize是16k,所以,数据上限 64TB=2^32 x 16k = 64TB。
64TB限制在MySQL官网也有提到:The minimum tablespace size is slightly larger than 10MB. The maximum tablespace size is four billion pages (64TB). This is also the maximum size for a table. 这段话来自:https://dev.mysql.com/doc/refman/5.5/en/innodb-restrictions.html
数据页(索引页)
数据页结构如下
名称 中文名 占用空间大小 简单描述 File Header
文件头 38
字节一些描述页的信息 Page Header
页头 56
字节页的状态信息 Infimum + Supremum
最小记录和最大记录 26
字节两个虚拟的行记录 User Records
用户记录 不确定 实际存储的行记录内容 Free Space
空闲空间 不确定 页中尚未使用的空间 Page Directory
页目录 不确定 页中的记录相对位置 File Trailer
文件结尾 8
字节校验页是否完整
记录在页中的存储
- 1.当一个记录需要插入页的时候,会从free space划分空间到user recorders
- 2.Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
记录头信息的秘密
- 1.预留位1 没有使用
- 2.预留位2 没有使用
- 3.delete_mask 标记该记录是否被删除
- 4.min_rec_mask B+树的每层非叶子节点中的最小记录都会添加该标记--``
- 5.n_owned 表示当前记录拥有的记录数,一般是每个页中的每个槽中最大值标识这个槽有几个记录。
- 6.heap_no 表示当前记录在记录堆的位置信息
- 7.record_type 表示当前记录的类型,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录
- 8.next_record 表示下一条记录的相对位置
delete_mask
- 1.这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了
- 2.被删除的记录还在页中,所有被删除掉的记录都会组成一个所谓的垃圾链表---可重用空间
- 3.如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
- 4.将这个delete_mask位设置为1和将被删除的记录加入到垃圾链表中其实是两个阶段
min_rec_mask
- 1.B+树的每层非叶子节点中的最小记录都会添加该标记(即目录页)
- 2.如果不是非叶子节点的最小记录则值为0,是的话就是1。
n_owned
- 1.一个数据页被分为多个组
- 2.每个组中的最后一条记录的n_owned标记该组有多少个记录
- 3.其他的记录该属性为0
heap_no
- 1.某页中的记录在该页中的位置
- 2.用户记录从2开始,0和1给最大和最小的2个虚拟记录。
- 3.记录大小的比较是比较主键的大小
伪记录的组成
- 1.记录头信息和固定的字节
- 2.字节代表的意思是,最小记录就是inifimum,最大记录就是supremum
record_type
- 1.这个属性表示当前记录的类型
- 2.0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录
next_record
- 1.从当前记录的真实数据到下一条记录的真实数据的地址偏移量
- 2.第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据
- 3.下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录
- 4.Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录
- 5.而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
- 6.最大记录的next_record的值为0,这也就是说最大记录是没有下一条记录
- 7.当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。
Page Directory(页目录)
页目录简单介绍
- 1.将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 2.每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 3.将每个组的最后一条记录的相对位置单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory
- 4.页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
- 5.一组对应一个槽
- 6.
对于最小记录所在的分组只能有 1 条记录
- 7.
最大记录所在的分组拥有的记录条数只能在 1~8 条之间
- 8.
剩下的分组中记录的条数范围只能在是 4~8 条之间
页目录中涉及到的分组
- 1.初始情况下一个数据页里只有最小记录和最大记录两条记录(上下边界),它们分属于两个分组。
2.Slot
是按照索引键值的顺序
进行逆序
存放(Infimum是下界,Supremum是上界
)- 3.之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个
- 4.在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
数据页中查找指定主键值的记录的过程分为两步(
B+Tree索引
本身并不能直接找到具体的一行记录
,只能找到该行记录所在的页
)
- 1.通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 2.通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Header(页面头部)
名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS |
2 字节 |
在页目录中的槽数量 |
PAGE_HEAP_TOP |
2 字节 |
第一个记录的地址 |
PAGE_N_HEAP |
2 字节 |
本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE |
2 字节 |
指向可重用空间的地址(就是标记为删除的记录地址) |
PAGE_GARBAGE |
2 字节 |
已删除的字节数,行记录结构中delete_flag 为1的记录大小总数 |
PAGE_LAST_INSERT |
2 字节 |
最后插入记录的位置 |
PAGE_DIRECTION |
2 字节 |
最后插入的方向 |
PAGE_N_DIRECTION |
2 字节 |
一个方向连续插入的记录数量 |
PAGE_N_RECS |
2 字节 |
该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID |
8 字节 |
修改当前页的最大事务ID,该值仅在二级索引中定义 |
PAGE_LEVEL |
2 字节 |
当前页在索引树中的位置,高度 |
PAGE_INDEX_ID |
8 字节 |
索引ID,表示当前页属于哪个索引 |
PAGE_BTR |
10 字节 |
非叶节点所在段的segment header,仅在B+树的Root页定义 |
PAGE_LEVEL |
10 字节 |
B+树所在段的segment header,仅在B+树的Root页定义 |
方向是指什么意思
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边
PAGE_N_DIRECTION注意点
- 假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计
File Header(文件头部)
IL_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值,独立表空间中都是0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID |
4 字节 |
页属于哪个表空间 |
- 页类型
名称 | 十六进制 | 描述 |
---|---|---|
FIL_PAGE_ALLOCATED |
0x0000 | 最新分配,还没使用 |
FIL_PAGE_UNDO_LOG |
0x0002 | Undo Log页 |
FIL_PAGE_INODE |
0x0003 | 段信息的节点 |
FIL_PAGE_IBUUF_FRE_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 | File Space Header |
FIL_PAGE_TYPE_XDES |
0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB |
0x000A | BLOB页 |
FIL_PAGE_INDEX |
0x45BF | B+树的子节点(索引页,也就是我们所说的数据页) |
File Trailer
- 1.为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况)
- 2.前4个字节代表页的校验和
- 3.后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
- 4.File Trailer与File Header类似,都是所有类型的页通用的。
前4个字节代表页的校验和
- 1.每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来
- 2.因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时校验和也会被写到页的尾部
- 3.如果完全同步成功,则页的首部和尾部的校验和应该是一致的
- 4.如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
各个数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
总结
-
InnoDB为了不同的目的而设计了不同类型的页,用于存放我们记录的页也叫做
数据页
。 -
一个数据页可以被分为7个部分,分别是
-
File Header
,表示文件头,占固定的38字节。 -
Page Header
,表示页里的一些状态信息,占固定的56个字节。 -
Infimum + Supremum
,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26
个字节。 -
User Records
:真实存储我们插入的记录的部分,大小不固定。 -
Free Space
:页中尚未使用的部分,大小不确定。 -
Page Directory
:页中的记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。 -
File Trailer
:用于检验页是否完整的部分,占用固定的8个字节。 -
每个记录的头信息中都有一个
next_record
属性,从而使页中的所有记录串联成一个单链表
。 -
InnoDB
会为把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽
,存放在Page Directory
中,所以在一个页中根据主键查找记录是非常快的,分为两步: -
通过二分法确定该记录所在的槽,找到该槽最小的记录。(槽对应的记录是这组数据里主键最大的值,可是页包含的记录是个单向链表怎么找到最小的值呢? 笨蛋,槽是挨着的。找到上一个槽的记录,这条记录的下一个位置就是当前槽的起始位置开始遍历,这个组最多8条遍历代价小)。
-
通过记录的next_record属性组成的链表遍历查找该槽中的各个记录。
-
每个数据页的
File Header
部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表
。 -
为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和
LSN
值,如果首部和尾部的校验和和LSN
值校验不成功的话,就说明同步过程出现了问题。