InnoDB 底层解析

InnoDB 是以页为基本单位在内存和磁盘间交互数据,页的大小一般为 16KB。

1. 行格式

记录在磁盘上的存储方式称为行格式。
有 4 种类型的行格式:

  1. Compact
  2. Redundant
  3. Dynamic
  4. Compressed

1.1 指定行格式

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称

1.2 Compact

在这里插入图片描述
在这里插入图片描述
变长字段:VARCHAR、VARBINARY、TEXT、BLOB。
NULL:每个允许存储 NULL 的列对应一个二进制位,为 1 表示该列为 NULL,为 0 表示该列不为 NULL。

记录头信息,由固定的 5 个字节组成:

  1. 预留位1 1 没有使用
  2. 预留位2 1 没有使用
  3. delete_mask 1 标记该记录是否被删除
  4. min_rec_mask 1 B+ 树的每层非叶子节点中的最小记录都会添加该标记
  5. n_owned 4 表示当前记录拥有的记录数
  6. heap_no 13 表示当前记录在页的位置信息
  7. record_type 3 表示当前记录的类型:0 表示普通记录,1 表示 B+ 树非叶子节点记录,2 表示最小记录,3 表示最大记录
  8. next_record 16 表示下一条记录的相对位置

隐藏列:

  1. DB_ROW_ID(row_id):非必须,6 字节,表示行 ID,唯一标识一条记录
  2. DB_TRX_ID:必须,6 字节,表示事务 ID
  3. DB_ROLL_PTR:必须,7 字节,表示回滚指针

主键生成策略:
优先使用用户自定义主键作为主键,如果用户没有自定义主键,则选取一个 Unique 键作为主键,如果表中连 Unique 键都没 定义的话,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键。

1.3 Redundant

Redundant 行格式是 MySQL5.0 之前用的一种行格式。

1.4 Dynamic、Compressed

MySQL 5.7 的默认行格式就是 Dynamic。
Dynamic 和 Compressed 和 Compact 挺像,只不过在处理行溢出数据时有所不同。
Compressed 和 Dynamic 不同的一点是,Compressed 会采用压缩算法对页面进行压 缩,以节省空间。

1.5 行溢出

一个页的大小是 16KB = 16384 字节,一个 VARCHAR 列可存储 65532 个字节,可能出现一个页存放不了一条记录的情况,就叫行溢出。

在 Compact、Redundant 中,如果数据大,只会存储该列前 768 字节,存储超出 768 字节的页面称为溢出页。
在 Dynamic、Compressed 中,如果数据大,把所有数据都存储在溢出中。

行溢出临界点:
规定最少存放 2 行记录,页本身占用 132 字节,每行记录额外有 27 字节,132 + 2*(27 + n) < 16384,n < 8099,这是在只存储一个列的情况下算出来的。

2. 页格式

在这里插入图片描述
File Header,文件头部,38 字节,页的一些通用信息
Page Header,页面头部,56 字节,数据页专有的一些信息
Infimum + Supremum,最小记录和最大记录,26 字节,两个虚拟的行记录
User Records,用户记录,大小不确定,实际存储的行记录内容
Free Space,空闲空间,大小不确定,页中尚未使用的空间
Page Directory,页面目录,大小不确定,页中的某些记录的相对位置
File Trailer,文件尾部,8 字节,校验页是否完整

2.1 User Records

当前记录被删除时,会修改记录头信息中的 delete_mask 为 1,也就是说被删除的记录还在页中,还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗。

被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。

同时我们插入的记录会记录自己在本页中的位置,写入了记录头信息中 heap_no 部分。heap_no 值为 0 和 1 的记录 InnoDB 自动给每个页增加的两个记录,称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录,这两条存放在页的 User Records 部分,他们被单独放在一个称为 Infimum + Supremum 的部分。

记录头信息中 next_record 记录了从当前记录的真实数据到下一条记录的真 实数据的地址偏移量。这其实是个链表,可以通过一条记录找到它的下一条记录。 但是需要注意注意再注意的一点是,下一条记录指得并不是按照我们插入顺序的 下一条记录,而是按照主键值由小到大的顺序的下一条记录。

在这里插入图片描述

2.2 Page Directory

Page Directory 主要是解决记录链表的查找问题。

在这里插入图片描述

  1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录) 划分为几个组。
  2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
  3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近 页的尾部的地方,这个地方就是所谓的 Page Directory。

一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
  2. 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

2.3 Page Header

这个部分占用固定的 56 个字节,专门存储各种状态信息:本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等。

2.4 File Header

它描述了一些针对各种页都通用的一些信息,比 方说页的类型,这个页的编号是多少,它的上一个页、下一个页是谁,页的校验 和等等,这个部分占用固定的 38 个字节。

页的类型,包括 Undo 日志页、段信息节点、Insert Buffer 空闲列表、Insert Buffer 位图、系统页、事务系统数据、表空间头部信息、扩展描述页、溢出页、 索引页。

同时通过上一个页、下一个页建立一个双向链表把许许多多的页就串联起来, 而无需这些页在物理上真正连着。

2.5 File Trailer

为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),InnoDB 每个页的尾部都加了一个 File Trailer 部分,这个部分由 8 个字 节组成,可以分成 2 个小部分:

  1. 前 4 个字节代表页的校验和
  2. 后 4 个字节代表页面被最后修改时对应的日志序列位置(LSN),这个也和校验页的完整性有关。

这个部分是和 File Header 中的校验和相对应的。每当一个页面在内存中修 改了,在同步之前就要把它的校验和算出来,因为 File Header 在页面的前边, 所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部, 如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿 断电了,那么在 File Header 中的校验和就代表着已经修改过的页,而在 File Trailer 中的校验和代表着原先的页,二者不同则意味着同步中间出了错。

File Trailer 与 File Header 类似,都是所有类型的页通用的。

3. 表空间

对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名 .ibd 的实际文件。

可以把表空间想象成被切分为许许多多个页的池子,当我们想为某个表插入一条记录的时候,就从池子中捞出一个对应的页来把数据写进去。

任何类型的页都有 File Header 这个部分,File Header 中专门的地方 (FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID)保存页属于哪个表空间,同时表空间中的每一个页都对应着一个页号(FIL_PAGE_OFFSET),这个页号由 4 个字节组成,也就是 32 个比特位,所以一个表空间最多可以拥有 2³² 个页,如果按照页的默认大小 16KB 来算,一个表空间最多支持 64TB 的数据。

3.1 独立表空间结构

在这里插入图片描述

3.1.1 区(extent)

对于 16KB 的页来说,连续的 64 个页就是一个区,也就是说一个区默认占用 1MB 空间大小。
每 256 个区又被划分成一个组。

第一个组最开始的 3 个页面的类型是固定的:用来登记整个表空间的一些整体属性以及本组所有的区被称为 FSP_HDR,也就是 extent 0 ~ extent 255 这 256 个区,整个表空间只有一个 FSP_HDR。

其余各组最开始的 2 个页面的类型是固定的,一个 XDES 类型,用来登记本组 256 个区的属性。FSP_HDR 类型的页面其实和 XDES 类型的页面的作用类似, 只不过 FSP_HDR 类型的页面还会额外存储一些表空间的属性。

引入区的主要目的是:
我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远,产生随机 I/O。

3.1.2 段(segment)

段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念。

我们提到的范围查询,其实是对 B+ 树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。

所以 InnoDB 对 B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成 2 个段,一个叶子节点段,一个非叶子节点段。

3.2 系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个 MySQL 进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些记录这些信息的页面,相当于是表空间之首,所以它的表空间 ID(Space ID)是 0。

系统表空间和独立表空间的前三个页面的类型是一致的,只是页号为 3~7 的页面是系统表空间特有的。分别包括:
在这里插入图片描述
系统表空间的 extent 1 和 extent 2 这两个区,也就是页号从 64~191 这 128 个页面被称为 Doublewrite buffer,也就是双写缓冲区。

3.2.1 双写缓冲区

双写缓冲区/双写机制是 InnoDB 的三大特性之一,还有两个是 Buffer Pool、 自适应 Hash 索引。

在把页写到数据文件之前,InnoDB 先把它们写到一个叫 doublewrite buffer(双写缓冲区)的连续区域内,在写 doublewrite buffer 完成后,InnoDB 才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB 在稍后的恢复过程中在 doublewrite buffer 中找到完好的 page 副本用于恢复。

InnoDB 的页大小一般是 16KB,其数据校验也是针对这 16KB 来计算的,将数据写入到磁盘是以页为单位进行操作的。而操作系统写文件是以 4KB 作为单位的,那么每写一个 InnoDB 的页到磁盘上,操作系统需要写 4 个块。 而计算机硬件和操作系统,在极端情况下(比如断电)往往并不能保证这一 操作的原子性,16K 的数据,写入 4K 时,发生了系统断电或系统崩溃,只有一 部分写是成功的,这种情况下会产生 partial page write(部分页写入)问题。这时页数据出现不一样的情形,从而形成一个"断裂"的页,使数据产生混乱。在 InnoDB 存储引擎未使用 doublewrite 技术前,曾经出现过因为部分写失效而导致数据丢失的情况。

doublewrite buffer 是 InnoDB 在表空间上的 128 个页(2 个区,extend1 和 extend2),大小是 2MB。为了解决部分页写入问题,当 MySQL 将脏数据 flush 到数据文件的时候, 先使用 memcopy 将脏数据复制到内存中的一个区域(也是 2M),之后通过这个内存区域再分 2 次,每次写入 1MB 到系统表空间,然后马 上调用 fsync 函数,同步到磁盘上。在这个过程中是顺序写,开销并不大,在完成 doublewrite 写入后,再将数据写入各数据文件,这时是离散写入。 所以在正常的情况下, MySQL 写数据页时,会写两遍到磁盘上,第一遍是写到 doublewrite buffer,第二遍是写到真正的数据文件中。如果发生了极端情况(断 电),InnoDB 再次启动后,发现了一个页数据已经损坏,那么此时就可以从 doublewrite buffer 中进行数据恢复了。

位于系统表空间上的 doublewrite buffer 实际上也是一个文件, 写系统表空间会导致系统有更多的 fsync 操作, 而硬盘的 fsync 性能因素会降低 MySQL 的整体性能。不过在存储上,doublewrite 是在一个连续的存储空间, 所以 硬盘在写数据的时候是顺序写,而不是随机写,这样性能影响不大,相比不双写, 降低了大概 5-10%左右。

所以,在一些情况下可以关闭 doublewrite 以获取更高的性能。比如在 slave 上可以关闭,因为即使出现了 partial page write 问题,数据还是可以从中继日志中恢复,比如有些文件系统本身就提供了部分写失效的防范机制,也可以关闭。

在数据库异常关闭的情况下启动时,都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到双写这个功能。

如果是写 doublewrite buffer 本身失败,那么这些数据不会被写到磁盘, InnoDB 此时会从磁盘载入原始的数据,然后通过 InnoDB 的事务日志来计算出正确的数据,重新写入到 doublewrite buffer,这个速度就比较慢了。如果 doublewrite buffer 写成功的话,但是写数据文件失败,innodb 就不用通过事务日志来计算了, 而是直接用 doublewrite buffer 的数据再写一遍,速度上会快很多。

3.2.2 InnoDB 数据字典

把记录插入对应索引的 B+树中时,MySQL 除了保存着我们插入的用户 数据之外,还需要保存许多额外的信息,比方说:

某个表属于哪个表空间,表里边有多少列,表对应的每一个列的类型是什么, 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面,该表有哪些外键,外键对应哪个表的哪些列,某个表空间对应文件系统上文件路径是什么。

上述这些数据并不是我们使用 INSERT 语句插入的用户数据,实际上是为了 更好的管理我们这些用户数据而不得已引入的一些额外数据,这些数据也称为元 数据。

InnoDB 存储引擎特意定义了一些列的内部系统表(internal system table) 来记录这些元数据:

在这里插入图片描述
这些系统表也被称为数据字典,它们都是以 B+树的形式保存在系统表空间的某些页面中,其中 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 这四个表尤其重要,称之为基本系统表。

用户是不能直接访问 InnoDB 的这些内部系统表的,除非你直接去解析系统 表空间对应文件系统上的文件。不过 InnoDB 考虑到查看这些表的内容可能有助 于大家分析问题,所以在系统数据库 information_schema 中提供了一些以 innodb_sys 开头的表:
在这里插入图片描述

4.Buffer Pool

InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。 将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。

InnoDB 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申 请了一片连续的内存,他们给这片内存起了个名,叫做 Buffer Pool(中文名是缓 冲池)。那它有多大呢?这个其实看我们机器的配置,默认情况下 Buffer Pool 只有 128M 大小。

show variables like ‘innodb_buffer_pool_size’;

在这里插入图片描述
可以在启动服务器的时候配置 innodb_buffer_pool_size 参数的值,它表示 Buffer Pool 的大小,就像这样:
[server]
innodb_buffer_pool_size = 268435456

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB。为了更好的管理这些在 Buffer Pool 中的缓存页,InnoDB 为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、 缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息,当然还有一些别的控制信息。

整个 Buffer Pool 对应的内存空间看起来就是这样的:
在这里插入图片描述
每个控制块大约占用缓存页大小的 5%,而我们设置的 innodb_buffer_pool_size 并不包含这部分控制块占用的内存空间大小,也就是说 InnoDB 在为 Buffer Pool 向操作系统申请连续的内存空间时,这片连续的内存空 间一般会比 innodb_buffer_pool_size 的值大 5%左右。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值