MySQL架构体系
MySQL是当今最通用的数据库软件之一,也是大部分人接触最多,时间最长的数据库软件之一。深入了解MySQL的架构和设计对于DBA,研发和运维都非常重要,能够帮助我们在日常工作中更好地理解和运用MySQL。
SQL语句在数据库底层的执行过程?MYSQL底层数据存储结构?MYSQL索引结构为什么使用b+树?MYSQL锁机制、种类和实现原理?Mysql事务是如何实现的
一、Mysql架构图整体介绍
首先先了解mysql的架构图,如下所示

从上面MySQL的架构图,可以看出MySQL的架构大致可以分为网络连接层、数据库服务层、存储引擎层和系统文件层四大部分。
1.1 网络连接层
MySQL架构体系的最上层是网络连接层,主要是客户端连接器。提供与MySQL服务器建立连接,几乎所有主流的服务端语言都支持,包括C、C++、Java、php等,都是通过各自的API接口与MySQL建立连接。
1.2 数据库服务层
数据库服务层是Mysql数据库服务器的核心,主要包括了连接器、查询缓存(mysql8已删除)、解析器、查询优化器和执行器等部分。所有跨引擎的功能也是在这一层

1.2.1 连接器
主要负责客户端和服务器建立连接,校验用户名和密码,连接池会存储和管理客户端与数据库的连接信息,连接池里的一个线程负责管理一个客户端到数据库的连接信息。
1.2.2 查询缓存
当数据库执行完一条sql语句时,会缓存它的结果(通过query_cache_type参数开启查询缓存)。再次执行同一条查询语句时,会直接从缓存里查询。但是MYSQL不推荐使用查询缓存,第一因为查询sql的语句必须完全相同,多一个空格,都会认为是一条不同的SQL语句,不命中缓存。第二是频繁失效,只要表结构或数据发生变化,缓存都会清空。所以mysql 8.0版本中直接删掉了查询缓存。
1.2.3 解析器
如果查询缓存没有命中,接下来就需要进入正式的查询阶段了。客户端程序发送过来的请求事实上只是一段文本而已,所以
MySQL服务器程序首先需要对这段文本做分析,判断请求的语法是否正确,然后从文本中将要查询的表、列和各种查询条件都提取出来,本质上是对一个SQL语句编译的过程,涉及词法解析、语法分析、语义分析等阶段。
1.2.3.1 词法解析
词法分析就是把一个完整的SQL语句分割成一个个的字符串,比如这条简单的SQL语句
select customer_id,first_name,last_name from customer where customer_id=14;
会被分割成10个字符串
select,customer_id,first_name,last_name,from,customer,where,customer_id,=,14
1.2.3.2 语法分析
分析器的第二步是根据词法分析的结果,语法分析器会根据语法规则做语法检查,判断你输入的这个SQL语句是否满足MySQL语法。如果你的语句不对,就会收到"You have an error in your SQL syntax"的错误提醒,比如下面这个语句select少打了开头的字母"s"。
mysql > elect customer_id,first_name,last_name from customer where customer_id =14;
ERROR 1064(42000) : You have an error in your SQL syntax;check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect customer_id,first_name,last_name from customer where customer_id = 14'at line 1

1.2.3.3 预处理器
预处理器则会进一步去检查解析树是否合法,比如表名是否存在,语句中表的列是否存在等等,在这一步MySQL会检验用户是否有表的操作权限。预处理之后会得到一个新的解析树。
1.2.4 查询优化器
在MySQL中,如果“解析树”通过了解析器的语法检查,此时就会由优化器将其转化为执行计划,然后选择一种最优的执行计划与存储引擎进行交互,通过存储引擎与底层的数据文件进行交互。Mysql使用的是基于成本模型的优化器,哪种执行计划成本最低就使用哪种(mysql选择它认为的成本小的,但成本小不意味着执行时间短)。
优化器都做了哪些优化呢?
1.当多个索引可用时,决定使用哪个索引;
2.重新定义表的关联顺序(多张表关联查询时,并不一定按照SQL中指定的顺序进行,但有一些技巧可以指定关联顺序)
3.提前终止查询(比如:使用Limit时,查找到满足数量的结果集后会立即终止查询)
4.优化MIN()和MAX()函数(找某列的最小值,如果该列有索引,只需要查找B+Tree索引最左端,反之则可以找到最大值)
5.优化排序(在老版本MySQL会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多)
1.2.5 执行器
MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,得到了一个查询计划。于是就进入了执行器阶段,开始执行语句。
(1)开始执行的时候,要先判断一下你对这个表customer有没有执行查询的权限,如果没有,就会返回没有权限的错误。(在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证)
(2)如果有权限,就使用指定的存储引擎打开表开始查询。执行器会根据表的引擎定义,去使用这个引擎提供的查询接口,提取数据。
1.3 存储引擎层
MySQL中的存储引擎层主要负责数据的写入和读取,与底层的文件进行交互。值得一提的是,MySQL中的存储引擎是插件式的,服务器中的查询执行引擎通过相关的接口与存储引擎进行通信,同时,接口屏蔽了不同存储引擎之间的差异。MySQL中,最常用的存储引擎就是InnoDB和MyISAM。
1.4 系统文件层
系统文件层主要包括MySQL中存储数据的底层文件,与上层的存储引擎进行交互,是文件的物理存储层。其存储的文件主要有:日志文件、数据文件、配置文件、MySQL的进行pid文件和socket文件等。
二、MySQL数据存储
2.1 MySQL磁盘文件介绍
MySQL在Linux中的数据索引文件和日志文件一般默认都在/var/lib/mysql目录下。
2.1.1 日志文件
2.1.1.1 错误日志(errorlog)
mycentos.err
默认的错误日志名称:hostname.err
2.1.1.2 二进制日志(bin log)
mysql-bin.000001
mysql-bin.000002
...
mysql-bin.000010
默认是关闭的
2.1.1.3 通用查询日志(general quey log)
默认情况下通用查询日志是关闭的,不建议开启。
2.1.1.4 慢查询日志(slow query log)
mysql-slow.log
2.1.1.5 重做日志文件(redo log)
ib_logfile0
ib_logfile1
2.1.1.6 回滚日志(undo log)
ibdata1
2.1.2 表结构文件
2.1.2.1 frm文件
user_innodb.frm
user_innodb.ibd
user_myisam.frm
user_myisam.myd
user_myisam.myi
MySQL数据的存储是基于表的,每个表都有一个对应的表结构文件。不论表使用的哪一种存储引擎,MySQL都会为表生成一个.frm为后缀名的文件,这个文件记录了这个表的表结构定义。
2.1.3 数据文件
存储引擎负责对表中数据的读取和写入,每个存储引擎会以自己的方式来保存表中的数据,在不同存储引擎中数据存放的方式一般是不同的。MySQL的数据文件存放在位置,可以通过参数datadir控制。
- 查看
MySQL数据文件:
SHOW VARIABLES LIKE‘%datadir%’;
2.1.3.1 InnoDB数据文件
1. ibd文件
使用独享表空间存储表数据和索引信息,一张表对应一个
.ibd文件。
2. ibdata文件
ibdata1
使用共享表空间存储表数据和索引信息,所有表共同使用一个或者多个
ibdata文件。
3. MyIsam数据文件
myd文件
主要用来存储表数据信息。
myi文件
主要用来存储表数据文件中任何索引的数据树。
2.2 MySQL索引结构介绍
InnoDB存储引擎逻辑存储结构可分为五级:表空间、段、区、页、行。


2.2.1 表空间
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、独占表空间、通用表空间、撤销表空间、临时表空间和Undo表空间等。
在InnoDB中存在两种表空间的类型:共享表空间和独立表空间。如果是共享表空间就意味着多张表共用一个表空间。如果是独立表空间,就意味着每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间可以在不同的数据库之间进行迁移。可通过命令
show variables like 'innodb_file_per_table';
查看当前系统启用的表空间类型。目前最新版本已经默认启用独立表空间。
- 如果开启了独立表空间
innodb_file_per_table=1,每张表一个单独的.ibd文件。 - 如果关闭了独立表空间
innodb_file_per_table=0,所有基于InnoDB存储引擎的表数据都会记录到系统表空间,文件名为ibdata1。
2.2.2 段
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。常见的段有数据段、索引段、回滚段等。
2.2.3 区
在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB。在任何情况下每个区大小都为1MB,为了保证页的连续性,InnoDB存储引擎每次从磁盘一次申请4-5个区。默认情况下,InnoDB存储引擎的页大小为16KB,即一个区中有64个连续的页。
2.2.4 页
页是InnoDB管理磁盘的最小单位,也是InnoDB中磁盘和内存交互的最小单位。每个页默认大小时是16KB,可以通过参数innodb_page_size将页的大小设置为4K、8K、16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以再次对其进行修改,除非通过mysqldump导入和导出操作来产生新的库。
索引树上一个节点就是一个页,MySQL规定一个页上最少存储2个数据项。如果向一个页插入数据时,这个页已将满了,就会从区中分配一个新页。如果向索引树叶子节点中间的一个页中插入数据,如果这个页是满的,就会发生页分裂。操作系统管理磁盘的最小单位是磁盘块,是操作系统读写磁盘最小单位,Linux中页一般是4K,可以通过命令查看。
innoDB存储引擎中,常见的页类型有:
- 数据页(B-tree Node)
- undo页(undo Log Page)
- 系统页 (System Page)
- 事物数据页 (Transaction System Page)
- 插入缓冲位图页(Insert Buffer Bitmap)
- 插入缓冲空闲列表页(Insert Buffer Free List)
- 未压缩的二进制大对象页(Uncompressed BLOB Page)
- 压缩的二进制大对象页 (compressed BLOB Page)
2.2.5 行
InnoDB的数据是以行为单位存储的,1个页中包含多个行。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200,即7992行记录。
了解了整体架构,下面我们开始详细对Page来做一些介绍。
先贴一张Innodb引擎中的Page完整的结构图

上面的概念实在太多了,为了方便理解,可以按下面的分解一下Page的结构

每部分的意义


页结构整体上可以分为三大部分,分别为通用部分(文件头、文件尾)、存储记录空间、索引部分。
第一部分通用部分,主要指文件头和文件尾,将页的内容进行封装,通过文件头和文件尾校验的CheckSum方式来确保页的传输是完整的。
在文件头中有两个字段,分别是FIL_PAGE_PREV和FIL_PAGE_NEXT,它们的作用相当于指针,分别指向上一个数据页和下一个数据页。连接起来的页相当于一个双向的链表,如下图所示:

需要说明的是采用链表的结构让数据页之间不需要是物理上的连续,而是逻辑上的连续。
第二个部分是记录部分,页的主要作用是存储记录,所以“最小和最大记录”和“用户记录”部分占了页结构的主要空间。另外空闲空间是个灵活的部分,当有新的记录插入时,会从空闲空间中进行分配用于存储新记录,如下图所示:

一个页内必须存储2行记录,否则就不是B+tree,而是链表了。
第三部分是索引部分,这部分重点指的是页目录(示意图2中的s0-sn),它起到了记录的索引作用,因为在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索,因此在页目录中提供了二分查找的方式,用来提高记录的检索效率。这个过程就好比是给记录创建了一个目录:
将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
第1组,也就是最小记录所在的分组只有1个记录;最后一组,就是最大记录所在的分组,会有1-8条记录;其余的组记录数量在4-8条之间。这样做的好处是,除了第1组(最小记录所在组)以外,其余组的记录数会尽量平分。在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。如下图所示:

页目录存储的是槽,槽相当于分组记录的索引。我们通过槽查找记录,实际上就是在做二分查找。这里我以上面的图示进行举例,5个槽的编号分别为0,1,2,3,4,我想查找主键为9的用户记录,我们初始化查找的槽的下限编号,设置为low = 0,然后设置查找的槽的上限编号high=4,然后采用二分查找法进行查找。
首先找到槽的中间位置p = (low + high) / 2 = (0 + 4) / 2 = 2,这时我们取编号为2的槽对应的分组记录中最大的记录,取出关键字为8。因为9大于8,所以应该会在槽编号为[p,high]的范围进行查找
接着重新计算中间位置p = (p + high) / 2 = (2 + 4) / 2 = 3,我们查找编号为3的槽对应的分组记录中最大的记录,取出关键字为12。因为9小于12,所以应该在槽3中进行查找。
遍历槽3中的所有记录,找到关键字为9的记录,取出该条记录的信息即为我们想要查找的内容。
2.3 索引介绍
2.3.1 InnoDB索引简介
InnoDB索引-官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.htm
2.3.1.1 主键索引
主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。InnoDB要求表必须有一个主键索引(MyISAM可以没有)。
2.3.1.2 辅助索引
除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。以表t_user_innodb的age列为例,age索引的索引结果如下图。
2.3.2 磁盘数据如何加载到InnoDB内存中
2.3.2.1 机械硬盘如何读取数据?
表中的数据是存储在磁盘文件上的,MySQL在处理数据时,需要先把数据从磁盘上读取到内存中。
1.一个硬盘一般由多个盘片组成,盘片的数量一般都在5片以内。

盘片的逻辑结构主要分为磁道、扇区和拄面。一个盘面被分为若干个磁道,每个磁道又被划分为多个扇区。扇区是磁盘存储的最小单位,大小是512字节。
下图显示的是一个盘面,盘面中一圈圈灰色同心圆环为一条条磁道,从圆心向外画直线,可以将磁道划分为若干个弧段,每个磁道上一个弧段被称之为一个扇区(图中绿色部分),每一个盘面有300~1024个磁道。

2.如何读取数据?
磁头要想读取数据,必须先根据磁盘地址找到对应的磁道,然后再等磁盘转到数据对应扇区后才能读取数据,一般会有十几毫秒的延迟。
读取步骤
传统机械硬盘读取数据的过程:
1.磁头移动到数据所在磁道。
2.磁盘旋转,将数据所在的扇区移至磁头之下。
3.磁盘继续旋转,所有所需的数据都被磁头从扇区中读出。
磁盘读取响应时间
磁盘的工作机制,决定了它读取数据的速度。读写一次磁盘信息所需的时间可分解为:寻道时间、延迟时间、传输时间。磁盘读取数据花费的时间,是这三个操作步骤所需时间之和。
1.寻道时间:第一步花费的时间,称为寻道时间。
寻道时间越短,I/O操作越快,目前磁盘的寻道时间一般都在10ms左右。
2.旋转延迟:第二步花费的时间,称为旋转延迟。
旋转延迟取决于磁盘转速,这一步相比寻道时间来说,比较快,远远小于1ms。
普通硬盘一般都是7200转/分,根据硬盘型号的不同,磁道离圆心的距离的不同,一个磁道包含几百个,几千个扇区,按100个扇区来算,旋转延迟为0.08ms(转一圈大约为8ms)。
3.数据传输时间:完成传输所请求的数据所需要的时间。
3.操作系统读取硬盘以磁盘块为单位
扇区是硬盘读写的最小单位,由于扇区的数量比较小,在寻址时花费的时间比较长,操作系统认为紧邻这个扇区的数据随后也是会被使用到,操作系统一般是以4KB的单位读取磁盘,读取后数据会被缓存在内存,称这个操作为预读。
4.MySQL读取以页为单位
MySQL本质上是一个软件,MySQL需要读取数据时,MySQL会调用操作系统的接口,操作系统会调用磁盘的驱动程序将数据读取到内核空间,然后将数据从内核空间copy到用户空间,随后MySQL就能从用户空间中读取到数据。操作系统读取磁盘时,Linux读取的最小单位一般为4K。最小单位由操作系统决定,不同的操作系统可能会有所不同。
MySQL的InnoDB存储引擎的数据读取以页为单位,也大小由参数innodb_page_size控制,默认值是16k。
2.3.1.2 InnoDB内存结构

2.4 MySQL是如何实现事务的?
2.4.1 原子性,持久性和一致性
原子性,持久性和一致性主要是通过redo log、undo log、Force Log at Commit和DoubleWrite机制来完成的。
redo log用于在崩溃时恢复数据
undo log用于对事务回滚时进行撤销,也会用于隔离性的多版本控制。
Force Log at Commit机制保证事务提交后redo log日志都已经持久化。
Double Write机制用来提高数据库的可靠性,用来解决脏页落盘时部分写失效问题。
2.4.2 InnoDB事务整体流程分析

2.4.3 使用redolog实现事务的一致性和持久性
2.4.3.1 内存数据落盘整体思路分析

2.4.3.2 CheckPoint检查点机制
- 当数据库发生宕机时,数据库不需要重做所有的日志,因为
Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间。 - 当缓冲池不够用时,根据
LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。 - 当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的。重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需要使用,那么必须强制
Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
2.4.3.3 DoubleWrite双写
如果说Insert Buffer给InnoDB存储引擎带来了性能上的提升,那么Double Write带给InnoDB存储引擎的是数据页的可靠性。

如上图所示,DoubleWrite由两部分组成,一部分是内存中的doublewritebuffer,大小为2MB,另一部分是物理磁盘上共享表空间连续的128个页,大小也为2MB。
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存中的double write buffer区域,之后通过double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免操作系统缓冲写带来的问题。在完成double write页的写入后,再讲double wirite buffer中的页写入各个表空间文件中。
如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的double write中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志。
2.4.4 使用MVCC实现事务的隔离性
2.4.4.1 回滚段/uodolog
根据行为的不同,undo log分为两种:insert undo log和update undo log
-
insert undo log:
是在
insert操作中产生的undo log。因为insert操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。 -
update undo log:
是
update或delete操作中产生的undo log。
因为会对已经存在的记录产生影响,为了提供MVCC机制,因此update undo log不能在事务提交时就进行删除,而是将事务提交时放到入history list上,等待purge线程进行最后的删除操作。
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undolog的并发写入和持久化。回滚段实际上是一种Undo文件组织方式。2.4.4.2 ReadView
对于使用
READUNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了。
对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。
对于使用READCOMMITTED和REPEATABLEREAD隔离级别的事务来说,就需要用到我们上边所说的版本链了。
核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。所以设计InnoDB的设计者提出了一个ReadView的概念,这个ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。
这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本(版本链中的版本)是否可见:
- 如果被访问版本的
trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。- 如果被访问版本的
trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。- 如果被访问版本的
trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
在MySQL中,READCOMMITTED和REPEATABLEREAD隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
2.5 MySQL是如何加行锁的?
2.5.1 RR隔离级别下的加锁机制

2.5.2 RC隔离级别下的加锁机制
间隙锁时为了解决幻读问题,在RC允许出现幻读现象所以RC隔离级别下行锁都加的是记录锁。只有在外键约束检查(foreign-key constraint checking)以及唯一键检查(duplicate-keychecking)时会使用间隙锁封锁区间。
参考文章
- MySQL体系结构
- MySQL结构体系
- MySQL是如何查询一条语句的