MySQL知识整合(一):InnoDB存储引擎
一、服务端的请求处理
当一条sql由MySQL客户端发往MySQL服务端时,它所经历的流程如下:
最终数据的读取和写入是由存储引擎负责的,下面来看下MySQL默认的存储引擎InnoDB的存储结构。
二、InnoDB存储结构
2.1:页
InnoDB的存储单元是页
,一页的大小一般是16KB
,可以通过系统变量innodb_page_size
控制,它的默认值是16384(字节),也就是16KB。我们获取数据时,InnoDB也是以页为单位进行传输的,页的详细介绍放到了下面第三节。
2.2:行&行结构
平时我们向表中插入一行数据,这一行数据是需要存放在磁盘上的,常见的存储格式有COMPACT
、REDUNDANT
、DYNAMIC
、COMPRESSED
,下面是设置语句:
1 | CREATE TABLE ${表名} ${列名} ROW_FORMAT = ${行格式名称} |
这些格式在原理上大体相同,下面主要以COMPACT
为切入点进行介绍。COMPACT行格式如下:
2.3:存储&数据溢出
页是以16kb为单位存储的,但我们的数据又是以行为单位插入的(行记录),页容量固定,但行记录的大小不可控,因此就导致了各种数据溢出问题。
2.3.1:列溢出
首先是单列溢出,如果有的列数据特别大,大到一页都放不下,那这一列就溢出了,COMPACT的做法是当前页只记录该列的少量数据,剩余的数据则分散的存到其他页中,然后在当前页花20字节记录下这些页的地址:
这里再说下DYNAMIC
和COMPRESSED
这两种行格式,它们跟COMPACT基本一致,只是在处理列溢出时的策略不太一样,DYNAMIC和COMPRESSED遇到超大列时并不会在当前页保存该列的少量数据,而是直接将真实数据分配到其他页中,当前页只记录其他页的地址,相比DYNAMIC,COMPRESSED还会压缩页数据,用来节省空间。
2.3.2:页溢出
再来看下页溢出,如何判定页是否溢出了呢?
页溢出临界判定:mysql规定正常情况下一页至少存储两行记录,假如我们行记录所需的真实数据存储上限为N,那么一页的数据构成就会是下面这样:
只有行记录所需空间满足上面这个式子(N < 8099),页才不会溢出,否则就会溢出,而对于溢出页,就不会再要求至少存储两行记录了,因而可以进一步做数据拆分来解决掉页溢出问题。
三、InnoDB页结构
3.1:基本结构
前面已经简单介绍了页、行、以及行的结构,本节就来重点讲下页的结构(为了便于理解,这里将图4
顺延了下来):
接下来详细的介绍下上图中的每一个部分。
3.2:记录-Free Space、User Records、Infimum+Supremum
如图5,页由7部分数据组成,其中User Records
是由Free Space
转化而来,每当我们往页里插入一条记录,就会从Free Space申请一块内存作为User Records存放这条记录,Free Space用完,就可以申请新的页了:
来个例子,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'); |
此时数据页的存储状态如下:
如果删除掉一条数据,那么这条数据的delete_flag会被标记为1,仅此而已,当插入新的数据时,会挤掉这条被删掉的老数据。
3.2:页目录-Page Directoy
当一张表中的记录值非常多时,要按照主键进行查询,为了保证查询效率,InnoDB又将上面的数据进行了分组,然后利用Page Directory
保存了这些分组最后一条数据的地址偏移量,并以此来定位一个分组,详情如下:
有了这些,根据主键查询的效率就得到了提升,具体过程:通过二分法
找到该分组对应的槽信息,然后通过邻槽的最后一条记录找到该槽对应组里主键值最小的记录,以这条记录开始,通过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+树的跟页面中定义 |
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 |
页类型 | 类型值 | 备注 |
---|---|---|
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.5:文件尾-File Trailer
InnoDB存储引擎会把数据存放在磁盘上,但磁盘速度很慢,所以InnoDB会以页为单位将数据载入到内存中处理,处理后的数据会再刷入磁盘中,如果在刷入磁盘的过程中发生意外(比如断电、死机),势必会导致严重的后果,为了避免这种情况的发生,InnoDB追加了File Trailer,结合File Header一起来校验页的完整性,它由8字节组成,分为两个部分: