InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上.
不论是读取数据还是写入数据, InnoDB 引擎是
以页为单位
操作的. 将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为
16
KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中
页内容组成:
一个页中至少存放两行记录.
每个页除了存放我们的记录以外,也需要存储一些额外的信息,乱七八糟的额外信息加起来需要
132
个字节的空间, 其他的空间都可以被用来存储记录。
每个记录需要的额外信息是
27
字节 (2 条记录则是 2*27)
包括:
2个字节用于存储真实数据的长度
1个字节用于存储列是否是NULL值
5个字节大小的头信息
6个字节的row_id列
6个字节的transaction_id列
7个字节的roll_pointer列
剩下的空间除以 2, 是单条记录在不是溢出列时的最大长度 ,
(16384 - 132 - 2*27)/2 = 8099
关于 Infimum + Supremum:
不管我们向页中插入了多少自己的记录,InnoDB 都规定他们定义的两条伪记录分别为
最小记录
与
最大记录
.
在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:
首先创建一个表:
mysql> CREATE TABLE page_demo( -> c1 INT, -> c2 INT, -> c3 VARCHAR(10000), -> PRIMARY KEY (c1) -> ) CHARSET=ascii ROW_FORMAT=Compact;Query OK, 0 rows affected (0.03 sec)
表的行结构如下:
头信息中的信息:
关于
delete_mask
的补充:这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了
(这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的
垃圾链表
,在这个链表中的记录占用的空间称之为所谓的
可重用空间
,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。)
将这个delete_mask位设置为 1 和将被删除的记录加入到垃圾链表中其实是
两个阶段
插入数据:
mysql> INSERT INTO page_demo VALUES(1, 100, ‘aaaa‘), (2, 200, ‘bbbb‘), (3, 300, ‘cccc‘), (4, 400, ‘dddd‘);Query OK, 4 rows affected (0.00 sec)Records: 4 Duplicates: 0 Warnings: 0
关于
next_record
的补充:文字补充:
从当前记录的真实数据到下一条记录的真实数据的地址偏移量.
下面我们看图说话:
从图中可以看出来,我们的记录按照主键从小到大的顺序形成了一个单链表。最大记录的next_record的值为0,这也就是说最大记录是没有下一条记录了,它是这个单链表中的最后一个节点。如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉:
DELETE FROM page_demo WHERE c1 = 2;
此时原来的记录变成了
记录的变化为:
第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1.
第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。
第1条记录的next_record指向了第3条记录。
最大记录的n_owned值从5变成了4
怎么从 page 中查出一条记录? 比如执行 SELECT * FROM page_demo WHERE c1 = 3;
这条语句.
innodb 对于查询的优化是使用了
页目录
这一设计. 也就是说每页里面有个跟目录相关的结构, 叫做槽(slot).
分槽以后的示意图如上图所示. 对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
1. 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
2. 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
3. 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
现在一口气添加数据:
mysql> INSERT INTO page_demo VALUES(5, 500, ‘eeee‘), (6, 600, ‘ffff‘), (7, 700, ‘gggg‘), (8, 800, ‘hhhh‘), (9, 900, ‘iiii‘), (10, 1000, ‘jjjj‘), (11, 1100, ‘kkkk‘), (12, 1200, ‘llll‘), (13, 1300, ‘mmmm‘), (14, 1400, ‘nnnn‘), (15, 1500, ‘oooo‘), (16, 1600, ‘pppp‘);Query OK, 12 rows affected (0.00 sec)Records: 12 Duplicates: 0 Warnings: 0
然后刚才的页记录就变成了这样了:
此时如果对页记录中的数据进行查询, 可以首先使用二分法, 找到数据所处于的槽, 然后顺序遍历槽中的元素找到对应的数据.
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的
56
个字节,专门存储各种状态信息
File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁, 这个部分占用固定的
38
个字节
FIL_PAGE_TYPE
这个代表当前页的类型,我们前边说过,InnoDB为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的数据页,其实还有很多别的类型的页,具体如下表:
FIL_PAGE_PREV
和 FIL_PAGE_NEXT
InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大(比方说一张表中可以有成千上万条记录),InnoDB可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,不过我们本集中唠叨的数据页(也就是类型为FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页其实是一个双链表,就像这样:
InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。
为了防止在同步过程中出现问题, InnoDB在每个页的尾部都加了一个 File Trailer部分,这个部分由 8 个字节组成,可以分成 2 个小部分:
1. 前4个字节代表页的校验和
2. 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
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 中,所以在一个页中根据主键查找记录是非常快的,分为两步:
通过二分法确定该记录所在的槽。
通过记录的 next_record 属性遍历该槽所在的组中的各个记录。
每个数据页的File Header部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表。
为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的LSN值,如果首部和尾部的校验和和LSN值校验不成功的话,就说明同步过程出现了问题。