穿透Mysql数据库底层:从 InnoDB 存储到 B + 树索引的深度解析(一)

前言

本期将开启新的专栏Mysql,深入了解Mysql的底层实现,理解Mysql,本期将先介绍MySQL概述,然后介绍InnoDB存储引擎记录行格式以及页结构。

MySQL概述

MySQL分为客户端程序和服务器程序,客户端程序需要用账号密码进行登录,然后我们在上面发送增删改查指令,服务器程序直接和我们存储的数据进行交互,并操作它们。我们看一下具体的流程,主要分为三个部分,连接管理解析与优化以及存储引擎
在这里插入图片描述

连接管理

客户端进程与服务器进程通过TCP/IP、命名管道等等通信方式建立连接,在客户端发起连接的时候,需要携带主机信息、用户名、密码,服务器程序会对客户端程序提供的信息进行验证,如果认证失败,拒绝连接。如果成功,连接建立,服务器线程将会一直等待客户端发送的请求。

解析与优化

现在我们的mysql服务器获得了文本形式的请求,然后对其进行解析与优化,包括查询缓存、语法解析以及查询优化。

查询缓存

mysql服务器程序在处理查询语句时,首先先到缓存中去查询是否有结果,如果直接命中,直接返回即可。
注意:
1 如果两个查询有任何字符上的,都不能缓存命中。
2 如果查询请求包含某些系统函数等等,那这个请求将不会被缓存。例如以函数NOW举例,就算请求一致,但是每个时刻的时间都不一样,所以每次查询的结果都不一样,如果缓存的话,就会查询错误。
3 同时只要表的结构发生变化,该表的缓存将从缓存系统删除。

由于查询缓存可能在某些时候优化查询性能,但是它不得不面临着维持缓存的巨大消耗,在MySQL 8.0中已将其删除。

语法解析

Mysql服务器程序在缓存没有命中后,需要先要对这个请求文本进行分析,判断语法是否准确,然后从文本中将要查询的表和查询条件提取出来放到Mysql服务器内部使用。

查询优化

Mysql服务器程序在获取需要的信息后,将对查询进行优化,比如指定一个最优的执行计划,先查询哪一个表等等。在其中又分为索引优化、查询语句优化以及数据库配置优化。

存储引擎

在完成查询优化后,Mysql服务器会把数据的存储和提取操作放到存储引擎中,我们知道表是一行一行的数据,如何将其存储在物理存储器上,如何从存储器里读数据等等都是存储引擎负责的,为了实现不同的功能,Mysql提供了各种各样的存储引擎,不同的存储引擎管理的表具体的存储结构可能不同,采用的存取算法也可能不同。我们看一些常用的存储引擎,不过Mysql最常用的就是InnoDB和MyISAM:
在这里插入图片描述
这些存储引擎向上边的Mysql server提供接口,在Mysql server完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的API,获取数据然后返回客户端即可。

设置表的存储引擎

我们
1 创建表时指定存储引擎

CREATE TABLE 表名( 
   建表语句; 
) ENGINE = 存储引擎名称;

2 修改表的存储引擎

ALTER TABLE 表名 ENGINE = 存储引擎名称;

接下来我们详细介绍一下InnoDB存储引擎。

InnoDB记录结构

InnoDB是将一个表中的数据存储在磁盘上的存储引擎,即使电脑关机,数据依然存在,真正处理数据是放在内存中,需要先将磁盘中的数据加载到内存中,如果处理的是写入或修改请求,需要将内存中的数据刷新到磁盘中。在这个过程,如果一条一条记录读取写入会非常慢,InnoDB采取的方式是:

将数据划分为若干个页,以页作为磁盘和内存的交互单位,InnoDB页的大小一般为16KB。

InnoDB行格式

在 MySQL 的 InnoDB 存储引擎中,行格式(Row Format)定义了数据行在磁盘上的存储方式。InnoD存储引擎包括四种行格式,CompactRedundantDynamicCompressed。不同的行格式在存储空间、性能和兼容性上有所差异。在创建或者修改表的语句中,我们可以指定需要的行格式:

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

1 Compact行格式
在这里插入图片描述
一条完整的记录可以被分为记录的额外信息记录的真实数据两个部分。
记录的额外信息包括变长字段长度列表、NULL值列表和记录头记录。是为了描述这条记录而不得不添加的一些信息。
1 变长字段长度列表
变长 字段长度列表逆序存储所有变长字段的实际长度,使用1或2字节表示。变长字段指的是Mysql支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M),各种TEXT类型,各种BLOB类型,我们将拥有这些数据类型的列称为变长字段。
注意:

  • 存储的时候是逆序,需要从后往前存储变长字段列的长度。
  • 如果该可变字段允许存储的最大字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使用2个字节,否则使用1个字节。

2 NULL值列表
我们知道表中某些列可能存储NULL值,如果将这些NULL放到记录真实数据中存储会占地方,所以Compact行格式把这些NULL的列统一管理起来,存储在NULL值列表中。我们用二进制位标记字段是否为 NULL,按字段顺序存储,不足 1 字节时补全为 1 字节,每个允许存储NULL字段的列对应以恶个二进制位,二进制位按照列的顺序逆序排列,当二进制为1时,表示该列的值为NULL,为0时,表示该列的值不为NULL。
3 记录头信息
记录头信息固定有5个字节组成,包含记录的状态信息(如是否为删除标记、是否为事务专用记录等)。
在这里插入图片描述
在这里插入图片描述
记录的真实数据除了包括自定义的列数据外,还会包括一些MySQL默认添加的列。
在这里插入图片描述
InnoDB存储引擎会为每条记录都添加 transaction_id和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)
2 Redundant行格式
在了解了Compact行格式后,其他行格式都是类似,接下来笔者简短进行介绍。

在这里插入图片描述

  • 字段长度偏移列表,Redundant行格式会把该条记录中所有列的长度信息按照逆序存储在字段长度偏移列表,偏移是指它是采用相邻数值得差值来计算各个列值得长度。举个例子: 25 24 1A 17 13 0C 06,首先先将逆序转为正序,06 0C 13 17 1A 24 25,然后按照两个相邻数值的差值来计算各个列值的长度:
    第一列的长度就是 0x06个字节,也就是6个字节。
    第二列的长度就是 (0x0C - 0x06)个字节,也就是6个字节,后面类似。
  • 与Compact 行格式的记录头信息相比,Redundant 行格式多了 n_field 和 1byte_offs_flag 这两个属性以及Redundant 行格式没有 record_type 这个属性。

在介绍剩余两个行格式之前,我们需要先了解一个概念,行溢出,Mysql规定一个页中至少存放两行记录,16KB大小是16384字节,而一个VARCHAR(M)类型得列最多可以存储65532个字节,这就会导致一个页存储不下一个记录,这时候就需要处理行溢出,将字符串得前768字节保留以及将剩余数据存储在其他页。
在这里插入图片描述
3 Dynamic 和 Compress行格式
他们的行格式和Compact行格式很像,不过在处理行溢出的时候会将真实数据所有字节存储在其他页面,不会像Compact行格式一个存储真实数据得前768个字节,以及溢出页地址。

InnoDB数据页结构

之前我们介绍了页,它是InnoDB的基本存储单位,不过对其结构没有深入了解,我们具体看一下它的组成结构。
在这里插入图片描述
一个InnoDB的数据页的存储空间大致分为7个部分,有些是确定的,有些是不确定的。
在这里插入图片描述
在页的7个组成部分中,我们存储的记录会按照我们指定的行格式(如果没有指定,按照Mysql不同版本有默认的行格式)存储到User Records部分,刚开始起始没有User Records这个部分,我们每插入一条记录,都会从free space 中申请一个记录大小的空间划分到User Records部分,当free Space空间全部用完后,如果还有新的记录,就需要申请新的页了。
在这里插入图片描述
为了更好的管理在User Records中这些记录,InnoDB需要利用在记录头中存储的额外信息。

记录头信息

我们先创建一个表,并往其中插入几条数据:

CREATE TABLE page_demo( 
c1 INT, 
c2 INT, 
c3 VARCHAR(10000), 
PRIMARY KEY (c1) 
CHARSET=ascii ROW_FORMAT=Compact; 
INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd'); 

我们把 c1 列指定为主键,所以在具体的行格式中InnoDB就没必要去创建那个所谓的 row_id 隐藏列了。
在这里插入图片描述
在这里插入图片描述
接下来我们具体看一下记录头记录中的各个属性:

  • delete_mask:这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。我们删除不是真正删除,只是打了个标记。
  • min_rec_mask:B+数的每层非叶子节点的最小记录都会添加该标记。
  • n_owned:在 InnoDB 的数据页里,记录并不是简单无序排列的,而是会被划分成多个组,每个组都有一个“组头记录”。它通常是组内主键值最大的记录,它的 n_owned 属性会记录该组内包含的记录数量(包含它自身)。
  • heap_no:这个属性表示当前记录在本页中的位置,0,1已经被分配掉,默认给InnoDB创建的两个伪记录,一个代表最小记录,一个代表最大记录,记录比较的时候比较的是主键的大小。由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records 部分,他们被单独放在一个称为Infimum + Supremum 的部分。
    在这里插入图片描述
  • record_type:这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。
  • next_record:这个属性表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。也就是通过可以找到下一条记录的地址。实际上就是一个链表。不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。
    在这里插入图片描述

Page Directory页目录

现在我们知道页中的记录是按主键从小到大排序串联成单链表,如果我们想查询的话,可以采取一种目录的方式,我们具体看一下:

  • 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned 属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory ,也就是 页目录 。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
    在这里插入图片描述
    规定:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1 - 8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
    分组步骤:
    初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
    在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
    如果需要在一个数据页查找指定主键值的记录的话就可以使用二分法:
    1 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
    2 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

Page Header页面头部

如果想知道本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,需要在页中定义了一个叫Page Header 的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门用于存储各种状态信息。
在这里插入图片描述

File Header(文件头部)

Page Header 是专门针对 数据页 记录的各种状态信息,比方说页里头有多少个记录了呀,有多少个槽了呀。我们现在描述的File Header 针对各种类型的页都通用,也就是说不同类型的页都会以File Header 作
为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁,这个部分占用固定的38个字节,是由下边这些内容组成的:
在这里插入图片描述

  • FIL_PAGE_PREV 和 FIL_PAGE_NEXT:FIL_PAGE_PREV 和 FIL_PAGE_NEXT分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来。注意:并不是所有类型的页都有上一个和下一个页的属性。
    在这里插入图片描述

File Trailer

在每个页的尾部都加了一个File Trailer 部分,这个部分由 8 个字节组成,可以分成2个小部分:

  • 前4个字节代表页的校验和
    这个部分是和File Header 中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header 在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header 中的校验和就代表着已经修改过的页,而在 File Trialer 中的校验和代表着原先
    的页,二者不同则意味着同步中间出了错。
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的。
    这个File Trailer 与 File Header 类似,都是所有类型的页通用的。

总结

本期主要介绍了MySQL的概述,让大家对于MySQL的底层有个大致的了解,同时主要介绍了InnoDB存储引擎存储记录的一些格式,包括页、行记录格式,页的结构。下期主要会介绍B+树索引,让你真正了解聚簇索引,二级索引,联合索引。

参考文献

MySQL是怎样运行的:从根儿上理解MySQL 小孩子4919

写在文末

 有疑问的友友,欢迎在评论区交流,笔者看到会及时回复

请大家一定一定要关注!!!
请大家一定一定要关注!!!
请大家一定一定要关注!!!
友友们,你们的支持是我持续更新的动力~

创作不易,求关注,点赞,收藏,谢谢~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wxchyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值