目录
扫描二维码关注公众号,回复:
13191034 查看本文章
MySQL服务器上负责对表中数据的读取和写入工作的部分是
存储引擎
,而服务器又支持不同类型的存储引擎,比如
InnoDB
、
MyISAM
、
Memory啥的,不同的存储引 擎一般是由不同的人为实现不同的特性而开发的,
真实数据在不同存储引擎中存放的格式一般是不同的
,甚至有的存储引擎比如
Memory都不用磁盘来存储数据,也就是说关闭服务器后表中的数据就消失 了。由于
InnoDB
是
MySQL默认的存储引擎,也是我们最常用到的存储引擎,我们也没有那么多时间去把各个存储引擎的内部实现都看一遍,了解了一个存储引擎的数据存储结构之后,其他的存储引擎都是依葫芦画瓢,等我们用到了再说哈~
InnoDB记录结构
InnoDB页简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入 或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录 从磁盘上读出来么?不,那样会慢死,
InnoDB
采取的方式是:
将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,
InnoDB
中页的大小一般为
16
KB
。也就是在一般情况下,一次最少从磁 盘中读取
16KB
的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
InnoDB行格式
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式
或者
记录格式
。设计
InnoDB
存储引擎的大叔们到现在为止设计了
4
种不同类型的
行格式,分别 是
Compact
、
Redundant
、
Dynamic
和Compressed行格式,随着时间的推移,他们可能会设计出更多的行格式,但是不管怎么变,在原理上大体都是相同的。
1.
页是
MySQL
中磁盘和内存交互的基本单位,也是
MySQL
是管理存储空间的基本单位。
2.
指定和修改行格式的语法如下:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
3InnoDB目前定义了4种行格式
-
COMPACT行格式
-
Redundant行格式
- Dynamic和Compressed行格式
这两种行格式类似于
COMPACT
行格式
,只不过在处理行溢出数据时有点儿分歧,它们不会在记录的真实数据处存储字符串的前
768
个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
另外,
Compressed
行格式会采用压缩算法对页面进行压缩。
一个页的大小一般是16KB
,当记录中的数据太多,当前页放不下的时候,会把多余的数据存储到其他页中,这种现象称为
行溢出
。
InnoDB数据页结构
1.InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页叫做数据页。
一个
InnoDB
数据页的存储空间大致被划分成了
7
个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。
- File Header,表示页的一些通用信息,占固定的38字节。
- Page Header,表示数据页专有的一些信息,占固定的56个字节。
- Infimum + Supremum,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26个字节。
- User Records:真实存储我们插入的记录的部分,大小不固定。
- Free Space:页中尚未使用的部分,大小不确定。
- Page Directory:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。
- File Trailer:用于检验页是否完整的部分,占用固定的8个字节。
3.
每个记录的头信息中都有一个next_record属性,从而使页中的所有记录串联成一个单链表。
min_rec_mask:
B+树的每层非叶子节点中的最小记录都会添加该标记。如果插入的某条记录的
min_rec_mask
值是
0
,意味着它不
是
B+
树的非叶子节点中的最小记录。
不论我们怎么对页中的记录做增删改操作,
InnoDB
始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的
。
4.
InnoDB
会为把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个
槽
,存放在
Page Directory
中,所以在一个页中根据主键查找记录是非常快的,分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Directory(页目录)
如果我们想根据主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:
SELECT * FROM page_demo WHERE c1 = 3;
我们平常想从一本书中查找某个内容的时候,一般会先看目录,找到需要查找的内容对应的书的页码,然后到对应的页码查看内容。设计
InnoDB的大叔们为我们的记录也制作了一个类似的目录,他们的制作过程是这样的:
- 1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录 中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
Page Header(页面头部)
设计
InnoDB
的大叔们为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header
的部分,它是
页
结构的第二部分,这个部分占用固定的
56
个字节,专门存储各种状态信息,具体各个字节都是干嘛的看下表:
- PAGE_DIRECTION
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是
PAGE_DIRECTION
。
- PAGE_N_DIRECTION
假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION这个状态表示。当然,如果最后一条记录的插入方向改变了的话, 这个状态的值会被清零重新统计。
File Header(文件头部)
File Header针对各种类型的页都通用,也就是说不
同类型的页都会 以File Header作为第一个组成部分
,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦吧啦吧啦~ 这个部分占用固定的38个字节,是由下边这 些内容组成的:
每个数据页的File Header部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表。
- FIL_PAGE_SPACE_OR_CHKSUM
这个代表当前页面的校验和(checksum
)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验 和
。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
- FIL_PAGE_OFFSET
每一个
页
都有一个单独的页号,就跟你的身份证号码一样,
InnoDB
通过页号来可以唯一定位一个
页
。
- FIL_PAGE_TYPE
这个代表当前
页
的类型,我们前边说过,
InnoDB
为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的
数据页
,其实还有很多别的类型的页,具体如下表:
我们存放记录的数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页。
- FIL_PAGE_PREV和FIL_PAGE_NEXT (重要)
这两个变量就是上文提到过的上一页指针和下一页指针,说是指针,是为了方便大家理解,实际上是页在磁盘上的偏移量。
InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大(比方说一张表中可以有成千上万条记录),
InnoDB可能不可以一次性为这么多数据分配一 个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,
FIL_PAGE_PREV
和
FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个
双向链表
把许 许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,
并不是所有类型的页都有上一个和下一个页的属性
,不过我们本集中所提到的的
数据页
(也就是类型为
FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页其实是一个双链表,就像这样:
File Trailer
InnoDB
存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以
页
为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据
同步
到磁盘
中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?
为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的LSN值,如果首部和尾部的校验和和LSN值校验不成功的话,就说明同步过程出现了 问题。
- 前4个字节代表页的校验和
这个部分是和
File Header
中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为
File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写 完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在
File Header
中的校验和就代表着已经修改过的页,而在
File
Trialer
中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个部分也是为了校验页的完整性的,只不过我们目前还没说
LSN
是个什么意思,所以大家可以先不用管这个属性。
总结反思时刻
一,作者讲了什么?
本文围绕InnoDB引擎的记录结构的数据页结构,分析其存储方式
二,作者是怎么把这事给讲明白的?
总分的形式,先放一张数据页的结构示意图, 一个 InnoDB 数据页的存储空间分成 7 个部分,对于每个部分的存放的内容列了很多表。
三,为了讲明白,作者讲了哪些要点,有哪些亮点?
- Page Directory:二分查找
- Page Header:数据页中存储记录的状态。 专门针对数据页记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽了呀。
- File Header: 针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦吧啦吧啦~
File Trailer:双链表
四,对作者所讲,我有哪些发散性思考?
- 在讲min_rec_mask 时提到了B+树,后续跟B+树的存储结构联系起来。
乔布斯在斯坦福大学的演讲,摆一下原文: “You can'tconnect the dots looking forward; you can only connect them looking backwards. So you haveto trust that the dots willsomehowconnect in your future.You haveto trust in something - your gut, destiny, life, karma, whatever. Thisapproach has never letme down,and it has madeallthe differenceinmy life.”上边这段话纯属心血来潮写的,大意是坚持做自己喜欢的事儿,你在做的时候可能并不能搞清楚这些事儿对自己之后的人生有啥影响,但当你一路走来回头看时,一切都 是那么清晰,就像是命中注定的一样。
五,将来在哪些场景,我能够使用到它?
参考资料:
- 《MySQL 是怎样运行的:从根儿上理解 MySQL - 快捷方式》——_盛放记录的大盒子 —— InnoDB 数据页结构