MySQL知识整合(一):InnoDB存储引擎

一、服务端的请求处理

当一条sql由MySQL客户端发往MySQL服务端时,它所经历的流程如下:

图1

最终数据的读取和写入是由存储引擎负责的,下面来看下MySQL默认的存储引擎InnoDB的存储结构。

二、InnoDB存储结构

2.1:页

InnoDB的存储单元是,一页的大小一般是16KB,可以通过系统变量innodb_page_size控制,它的默认值是16384(字节),也就是16KB。我们获取数据时,InnoDB也是以页为单位进行传输的,页的详细介绍放到了下面第三节。

2.2:行&行结构

平时我们向表中插入一行数据,这一行数据是需要存放在磁盘上的,常见的存储格式有COMPACTREDUNDANTDYNAMICCOMPRESSED,下面是设置语句:

1
2
CREATE TABLE ${表名} ${列名} ROW_FORMAT = ${行格式名称}
ALTER TABLE ${表名} ROW_FORMAT = ${行格式名称}

这些格式在原理上大体相同,下面主要以COMPACT为切入点进行介绍。COMPACT行格式如下:

图2

2.3:存储&数据溢出

页是以16kb为单位存储的,但我们的数据又是以行为单位插入的(行记录),页容量固定,但行记录的大小不可控,因此就导致了各种数据溢出问题。

2.3.1:列溢出

首先是单列溢出,如果有的列数据特别大,大到一页都放不下,那这一列就溢出了,COMPACT的做法是当前页只记录该列的少量数据,剩余的数据则分散的存到其他页中,然后在当前页花20字节记录下这些页的地址:

图3

这里再说下DYNAMICCOMPRESSED这两种行格式,它们跟COMPACT基本一致,只是在处理列溢出时的策略不太一样,DYNAMIC和COMPRESSED遇到超大列时并不会在当前页保存该列的少量数据,而是直接将真实数据分配到其他页中,当前页只记录其他页的地址,相比DYNAMIC,COMPRESSED还会压缩页数据,用来节省空间。

2.3.2:页溢出

再来看下页溢出,如何判定页是否溢出了呢?

页溢出临界判定:mysql规定正常情况下一页至少存储两行记录,假如我们行记录所需的真实数据存储上限为N,那么一页的数据构成就会是下面这样:

图4

只有行记录所需空间满足上面这个式子(N < 8099),页才不会溢出,否则就会溢出,而对于溢出页,就不会再要求至少存储两行记录了,因而可以进一步做数据拆分来解决掉页溢出问题。

三、InnoDB页结构

3.1:基本结构

前面已经简单介绍了页、行、以及行的结构,本节就来重点讲下页的结构(为了便于理解,这里将图4顺延了下来):

图5

接下来详细的介绍下上图中的每一个部分。

3.2:记录-Free Space、User Records、Infimum+Supremum

如图5,页由7部分数据组成,其中User Records是由Free Space转化而来,每当我们往页里插入一条记录,就会从Free Space申请一块内存作为User Records存放这条记录,Free Space用完,就可以申请新的页了:

图6

来个例子,page_demo表有三个属性,分别是c1(主键)、c2(int)、c3(varchar),以COMPACT格式存储,这时王这张表里新增四条记录:

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

此时数据页的存储状态如下:

图7

如果删除掉一条数据,那么这条数据的delete_flag会被标记为1,仅此而已,当插入新的数据时,会挤掉这条被删掉的老数据。

3.2:页目录-Page Directoy

当一张表中的记录值非常多时,要按照主键进行查询,为了保证查询效率,InnoDB又将上面的数据进行了分组,然后利用Page Directory保存了这些分组最后一条数据的地址偏移量,并以此来定位一个分组,详情如下:

图8

有了这些,根据主键查询的效率就得到了提升,具体过程:通过二分法找到该分组对应的槽信息,然后通过邻槽的最后一条记录找到该槽对应组里主键值最小的记录,以这条记录开始,通过next_record一直往下遍历(单链表遍历),匹配要查的主键。二分查找法的时间复杂度为O(logN)

3.3:页头-Page Header

页头主要用来存储页的一些状态信息,这些信息如下:

名称 大小 备注
PAGE_N_DIR_SLOTS 2bytes 页目录内的槽数量
PAGE_HEAP_TOP 2bytes 还未使用的空间最小地址,该地址后就是Free Space
PAGE_N_HEAP 2bytes 首bit标记本页是否为紧凑型,剩余15bit表示本页的总记录数(包含Infimum+Supremum、被删除数据)
PAGE_N_RECS 2bytes 本页用户记录的数量(不包含Infimum+Supremum、被删除数据)
PAGE_FREE 2bytes 被删记录同样会通过next_record组成一个“废弃链表”,这些记录空间可被重复利用,PAGE_FREE就是链表头对应记录在页中的偏移量
PAGE_GARBAGE 2bytes 已删除记录所占字节数
PAGE_LAST_INSERT 2bytes 最后插入记录的位置
PAGE_DIRECTION 2bytes 记录插入的方向(新插入记录的主键值比上一条记录大,此时插入方向视为右插,否则是左插,即左小右大,PAGE_DIRECTION就用来记录最后插入数据的插入方向)
PAGE_N_DIRECTION 2bytes 同一个方向连续多次插入记录时,会用该字段计数,如果后续插入方向发生变化,便会清零重计
PAGE_MAX_TRX_ID 8bytes 修改本页的最大事务id,该值仅在二级索引页面中定义
PAGE_LEVEL 2bytes 本页在B+树中所处的层级
PAGE_INDEX_ID 8bytes 索引ID,表示本页属于哪个索引
PAGE_BTR_SEG_LEAF 10bytes B+树叶子节点段的头部信息,仅在B+树的跟页面中定义
PAGE_BTR_SEG_TOP 10bytes B+树非叶子节点段的头部信息,仅在B+树的跟页面中定义
表1

3.4:文件头-File Header

如果说Page Header是用来描述页内记录的状态,那么File Header则用来记录页本身的信息,这些信息如下:

名称 大小 备注
FIL_PAGE_SPACE_OR_CHKSUM 4bytes mysql版本 < 4.0.14:表示本页所在的表空间ID;mysql版本 >= 4.0.14:表示本页的校验和
FIL_PAGE_OFFSET 4bytes 页号(即页ID,InnoDB通过页号来确定一个页);当要存放的数据本身很大,以至于出现了前面所说的溢出现象,这时就需要多个页存放这些数据了,聚合这些数据最简单的办法就是通过页号将它们串连成一个双向链表,下方的两个属性就是用来干这个的
FIL_PAGE_PREV 4bytes 上一页的页号
FIL_PAGE_NEXT 4bytes 下一页的页号
FIL_PAGE_LSN 8bytes 本页被最后修改时对应的LSN值(日志序列号)
FIL_PAGE_TYPE 2bytes 本页的类型,InnoDB为了不同的目的将页分成了好几种,具体的类型以及类型值详见表3
FIL_PAGE_FILE_FLUSH_LSN 8bytes 仅在系统表空间的第一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4bytes 本页所在的表空间ID
表2
页类型 类型值 备注
FIL_PAGE_TYPE_ALLOCATED 0x0000 新分配,还未使用
FIL_PAGE_UNDO_LOG 0x0002 undo日志页
FIL_PAGE_INODE 0x0003 存储段的信息
FIL_PAGE_IBUF_FREE_LIST 0x0004 Change Buffer空闲链表
FIL_PAGE_IBUF_BITMAP 0x0005 Change 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 溢出页
FIL_PAGE_INDEX 0x45BF 索引页(也就是存放我们业务数据的页,之前例子中的页就是这种)
表3

3.5:文件尾-File Trailer

InnoDB存储引擎会把数据存放在磁盘上,但磁盘速度很慢,所以InnoDB会以页为单位将数据载入到内存中处理,处理后的数据会再刷入磁盘中,如果在刷入磁盘的过程中发生意外(比如断电、死机),势必会导致严重的后果,为了避免这种情况的发生,InnoDB追加了File Trailer,结合File Header一起来校验页的完整性,它由8字节组成,分为两个部分:

图9