Compact行格式

数据结构

1.变长字段长度列表:用来记录变长字段实际存储数据的长度

2.NULL值列表:用来记录允许为null的字段列表,通过0或1来标记某字段是否为null

3.记录头信息:5个字节。

· 预留位:头两位是两个预留位,保留。

· delete mask:1位。标记当前记录是否被删除了。

· min rec mask:1位。标记当前记录是否是B+树中非叶子节点的最小记录。

· n owned:该记录槽所拥有的记录数量。

· heap no:该记录在堆中的位置信息。

· record_type:普通数据000,是非叶节点的001,最小伪记录010,最大伪记录011,1xx保留。

· next_record:指向页内下一条记录的指针。

4.DB_ROW_ID:主键ID,不一定会生成。如果没定义主键且没有唯一索引,此列作为隐藏主键。

5.DB_TRX_ID:可以理解为事务的版本号。每产生一个事务,该值自增。

6.DB_ROLL_PTR:undo log的指针。回滚时通过该指针从对应的undo log中找到原始数据。

7.列数据:记录了真实数据各列的值。

存储

对于定长字段,如果数据长度不足,则需要填充,针对数据类型补0或补空格。

对于变长字段,保存实际的数据即可。如果变长字段发生了修改,一般是将当前记录标记为删除,再在其后新生成一条记录来存放更新后的数据。提高了更新速度,但产生了存储碎片。

如果某列发生了数据溢出,那么,只在该列存放内容的前768个字节和20个字节的指针,用来指向分散到溢出页中的剩下的内容。


索引页

页是innoDB引擎内存管理的最小单元,也是内存和磁盘交互的最小单元。索引页是页的一种,是对存储数据操作的最小单元,默认大小是16kb。

结构

1.File Header:页的通用结构。记录了页的一些通用状态的信息。

· FILE_PAGE_SPACE_OR_CHECKSUM:校验和。页的尾部也有一个校验和,头尾两个校验和一致时,这个页的数据才是没有损坏的。

· FILE_PAGE_OFFSET:页号,页的唯一标识,全局递增管理。

· FILE_PAGE_PREV & FILE_PAGE_NEXT:前页和后页的指针。可以看出页的双向链表格式。

· FILE_PAGE_LSN:事务的日志序列号,全局唯一。记录页最后一次被修改时的日志号。

· FILE_PAGE_TYPE:页的类型。索引页固定是0x45BF。

· FIL_PAGE_FILE_FLUSH_LSN:仅在第一个页中使用,记录文件至少被刷到了哪个LSN值。

· FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:页所属表空间。

2.Page Header:索引页特有结构。记录了索引页的相关状态信息。

· PAGE_N_DlR_SLOTS:槽数量。为了加快页内检索效率,数据分组存放,每个组就是一个槽。

· PAGE_HEAP_TOP:Free Space的起始位置。

· PAGE_N_HEAP:页内存放的记录数。包含虚拟记录和删除记录。

· PAGE_FREE:指向删除记录链表的头部。

· PAGE_DIRECTION:最后一条记录插入的方向。比上一条大右边,否则左边。

· PAGE_N_DIRECTION:同一方向连续插入的记录数。

· PAGE_LEVEL:当前页在B+树中所处层级。

· PAGE_BTR_SEG_LEAF:B+树叶子段的头部信息,仅在B+树的Root页定义。

· PAGE_BTR_SEG_TOP:B+树非叶子段的头部信息,仅在B+树的Root页定义。

3.User Records:存放的真实数据,单链表结构。

4.Infimum和Supremum:是索引页内两条虚拟记录,位于User Reords链表的头和尾。

5.Page Directory:页目录,用于提高页内数据检索效率。

将页内非删除的记录划分为N个组,把该组的记录数记录到该组的最大记录的n_owned字段中,并将每个组中最大的记录的地址偏移量提取出来,从File Trailer往前写,每个地址偏移量占2个字节,称作一个槽。这些槽构成了页目录。Infimum单独成组,Supremum所在组允许1~8条记录,其它组为4~8条。通过二分法快速定位到槽,再对组进行遍历来提高效率。

6.Free Space:剩余的未分配的内存大小。

6.File Trailer:页的通用结构,用于页的完整性校验。

刷盘前根据页数据计算出一个校验和放在页头和页尾,页刷屏时先刷页头最后刷页尾,当头尾两个校验和不一致时,那肯定是刷到一半出错了。还记录了页最后一次修改时的LSN的后四个字节,正常情况下应该和页头的FILE_PAGE_LSN的后四个字节相同。

页是通过页头记录的前后页指针来构建页的连续性,如果不做优化的话,那作为一个单纯的链表结构,在内存中的分布是离散的,这对InnoDB引擎而言就意味着,即使是逻辑上连续的页,也很可能需要使用随机IO。区概念的提出,就是为了优化这个问题,说白了其实就是将逻辑上连续的页尽可能的放置在物理上连续的内存空间里,以达到顺序IO的目的。

在物理上连续的64个页构成一个区,16KB*64=1M。连续的256个区构成一个组,256M。

但是区并不会一开始就作为段内最小的内存分配单位,为了不在小表或者undo类的段上浪费内存,分配策略是将数据优先存储在32个离散页上,如果存满了才会开始区的直接分配。

区有FREE、FREE_FRAG、FULL_FRAG、FSEG四种状态。只有第四种状态的区是隶属于某个段的,其它状态只属于表空间。

XDES Entry

通过XDES节点来管理记录区的基本信息。主要记录了区隶属的段、区的状态、XDES节点的前后节点指针、区的状态位图。而XDES节点也是存储在页里的,在物理结构上是连续的,所以根本不需要再浪费额外的内存空间来记录这个前后节点指针,所以这个指针的连续含义是根据区的状态串联起来的一个链表。

XDES页

专门存放XDES节点的页,该页为每个组内固定的第一页。组由256个连续的区组成,也就对应了256个XDES节点。

XDES Entry链表

因为XDES Entry本身就已经根据区的状态串联了链表,那根据区的隶属和状态就可以分成很多条链表。

表空间:

· FREE链表:将所有FREE状态的区连起来的链表

· FREE_FRAG链表:将所有FREE_FRAG状态的区连起来的链表

· FULL_FRAG链表:将所有FULL_FRAG状态的区连起来的链表.

所以表空间内的内存分配策略前期的离散页,就是从FREE_FRAG链表中拿出一个区开始分配,这个区用完了就将状态改为FULL_FRAG,并从FREE_FRAG链表中移除并加到FULL_FRAG链表。如果FREE_FRAG链表没有节点了,就从FREE链表中取一个节点。

段:

· FREE链表:隶属于同一个段所有未使用过的区连起来的链表

· NOT_FREE链表:隶属于同一个段所有使用过但还有空余的区连起来的链表

· FULL链表:隶属于同一个段所有使用过且没有空余的区连起来的链表

一个索引有两个段,也就是说,每个索引都有6个链表。段内的内存分配策略和表空间的一致。

链表基节点

所以每个表都会有至少9个链表,当插入一条数据时,就需要找到对应的链表,从符合分配策略的区内取出一个页来存储记录。链表基节点的提出就是为了在类似场景下能够定位到对应的链表。

每个链表基节点对应一条链表,记录了该链表的个数和头尾XDES节点的指针,16个字节。

表空间:链表基节点存放在第一个组的第一个页面的页头里,16*3存储了三个链表基节点。

段:innoDB为每个段创建了Inode节点,隶属于段的3个链表对应的链表基节点就存储在这里。

段只是一个逻辑上的概念,由32个零散页和若干个区组成。

根据B+树只在叶子节点存真实的数据记录,非叶节点存目录项记录的特性,每个索引根据记录的类型分为两个段来分开存储,来保证各自顺序扫描的性能。

INODE Entry:

为了管理记录段的信息提出的数据结构。

· Segment ID:段ID。

· NOT_FULL_N_USED:NOT_FULL链表里已使用页的数目。

· LIST_BASE_NODE:链表基节点。

· FRAGMENT_ARRAY_ENTRY:32个零散页的存储位置,记录的页号。

INODE页:

专门存放INODE节点的页。其特有的属性为:指向上/下一个INODE页的指针、最多可存放85个INODE节点的内存区域。

INODE页链表:

当表中段特别多,就需要多个INODE页来存储,根据INODE页是否有空余分成两个链表来将INODE页串联起来。

INODE节点链表基节点:

INODE节点链表基节点的提出是为了定位到相应的链表。和表空间的链表基节点一样,固定存储在第一个组的第一页上。

Segment Header:

该数据结构的提出是为了定位到段。在索引Root页的Page Header中,PAGE_BTR_SEG_LEAF和PAGE_BTR_SEG_TOP两个属性其实就是Segment Header。

综上,

当给一张表建一个索引时,

1.根据表空间的INODE节点链表基节点找到INODE页的NOT_FULL链表

2.从该NOT_FULL链表中取一个INODE页

3.往该INODE页中加入两个INODE节点,对应该索引的两个段

4.如果该INODE页添加INODE节点后满了,则将该INODE页串到FULL链表

当需要往一张表里插入一条数据时(假设都是索引键自增插入),其实对每个B+树来说都是要在两个段中插入一条数据。根据B+树Root页找到对应的两个INODE节点,也就是段,对每个段执行以下操作:

1.如果段的32个离散页没满,直接存在离散页中;否则,找到各个段对应的链表基节点,从而找到对应的3条XDES链表。

2.从NOT_FREE链表中取一个XDES页

3.在取出的XDES页中取一个XDES节点,也就是区,在该区中插入一条数据

表空间

在innoDB引擎中数据都按表空间来组织存储的。也就是说,数据的物理存储是以表空间为单位。

包括系统表sys、临时表、配置表、undo表等等。

如果想要每个表有一个单独的表空间,修改innoDB的配置innodb_file_per_table值为on就可以了。独立表空间只存放该表的索引、insert buffer bitmap和对应的数据。其它的诸如undo信息、insert buffer索引页、double write buffer等信息依旧存储在默认表空间里,也就是共享表空间。这么设置的好处在于表的独立性,比如某个表的表空间损坏了不会影响其它表,快速备份或还原某个表文件不会中断其它表的使用;坏处在于额外消耗的系统性能,如果一个表空间的文件单次fsync系统调用就能将数据落盘,在拆分成多个单独的表空间后就意味着多次的fsync调用。

undo信息并不一定存放在undo表空间,它默认存放在系统表空间的,但这是可以配置的。但是决定放在哪个表空间里,取决于存储卷的类型,如果是SSD存储,推荐存放在undo表空间。

表碎片清理和表空间收缩

碎片产生的原因

1.从表中删除一条记录,记录并不会真的从表中删除,而是将该行的delete mask设置为true;当修改某条数据的变长字段时,也只是将将该行的delete mask设置为true。也就是说,在这两种情况下,初始的数据并不会将它们所占用的空间释放掉。

2.从表中插入一条数据,MySQL会尝试使用空白空间,但如果某个空白空间一直没有被合适大小的数据占用,就会形成里碎片。

碎片产生的影响

MySQL对数据进行扫描时,扫描的对象其实是列表的容量上限,也就是说,即使是一张占用了1GB但有效数据只有1KB的表,仍会被当作是1GB的表进行处理。所以碎片越多,会降低访问表时的IO,降低查询性能。

碎片清理

执行OPTIMIZE TABLE + 表名的语句。但是该命令会造成锁表,且数据量越大的表,锁表的时间就越久。MySQL官方建议不要经常(每小时或每天)进行碎片清理,根据表的数据是否经常被删除、表的定长字段是否经常修改等实际情况,一周或一个月清理一次即可。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐