MySQL架构体系


MySQL是当今最通用的数据库软件之一,也是大部分人接触最多,时间最长的数据库软件之一。深入了解MySQL的架构和设计对于DBA,研发和运维都非常重要,能够帮助我们在日常工作中更好地理解和运用MySQL

  • SQL语句在数据库底层的执行过程?
  • MYSQL底层数据存储结构?
  • MYSQL索引结构为什么使用b+树?
  • MYSQL锁机制、种类和实现原理?
  • Mysql事务是如何实现的

一、Mysql架构图整体介绍

首先先了解mysql的架构图,如下所示

从上面MySQL的架构图,可以看出MySQL的架构大致可以分为网络连接层、数据库服务层、存储引擎层和系统文件层四大部分。

1.1 网络连接层

MySQL架构体系的最上层是网络连接层,主要是客户端连接器。提供与MySQL服务器建立连接,几乎所有主流的服务端语言都支持,包括CC++Javaphp等,都是通过各自的API接口与MySQL建立连接。

1.2 数据库服务层

数据库服务层是Mysql数据库服务器的核心,主要包括了连接器、查询缓存(mysql8已删除)、解析器、查询优化器和执行器等部分。所有跨引擎的功能也是在这一层
3c6f000500e9f1f5b697.jpg

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

20210709225156.png

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中,最常用的存储引擎就是InnoDBMyISAM

1.4 系统文件层

系统文件层主要包括MySQL中存储数据的底层文件,与上层的存储引擎进行交互,是文件的物理存储层。其存储的文件主要有:日志文件、数据文件、配置文件、MySQL的进行pid文件和socket文件等。

二、MySQL数据存储

2.1 MySQL磁盘文件介绍

MySQLLinux中的数据索引文件和日志文件一般默认都在/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存储引擎逻辑存储结构可分为五级:表空间、段、区、页、行。
111lixajhavh.jpg
20200322063054451.png

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将页的大小设置为4K8K16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以再次对其进行修改,除非通过mysqldump导入和导出操作来产生新的库。

索引树上一个节点就是一个页,MySQL规定一个页上最少存储2个数据项。如果向一个页插入数据时,这个页已将满了,就会从区中分配一个新页。如果向索引树叶子节点中间的一个页中插入数据,如果这个页是满的,就会发生页分裂。操作系统管理磁盘的最小单位是磁盘块,是操作系统读写磁盘最小单位,Linux中页一般是4K,可以通过命令查看。

innoDB存储引擎中,常见的页类型有:

  1. 数据页(B-tree Node)
  2. undo页(undo Log Page)
  3. 系统页 (System Page)
  4. 事物数据页 (Transaction System Page)
  5. 插入缓冲位图页(Insert Buffer Bitmap)
  6. 插入缓冲空闲列表页(Insert Buffer Free List)
  7. 未压缩的二进制大对象页(Uncompressed BLOB Page)
  8. 压缩的二进制大对象页 (compressed BLOB Page)

2.2.5 行

InnoDB的数据是以行为单位存储的,1个页中包含多个行。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200,即7992行记录。

了解了整体架构,下面我们开始详细对Page来做一些介绍。

先贴一张Innodb引擎中的Page完整的结构图
hang.jpeg
上面的概念实在太多了,为了方便理解,可以按下面的分解一下Page的结构
pagestruct.jpg

每部分的意义
pagestruct21024x423.png

innodbpagestruct.jpg

页结构整体上可以分为三大部分,分别为通用部分(文件头、文件尾)、存储记录空间、索引部分。

第一部分通用部分,主要指文件头和文件尾,将页的内容进行封装,通过文件头和文件尾校验的CheckSum方式来确保页的传输是完整的。

在文件头中有两个字段,分别是FIL_PAGE_PREVFIL_PAGE_NEXT,它们的作用相当于指针,分别指向上一个数据页和下一个数据页。连接起来的页相当于一个双向的链表,如下图所示:
innodbpagestruct31024x237.jpg

需要说明的是采用链表的结构让数据页之间不需要是物理上的连续,而是逻辑上的连续

第二个部分是记录部分,页的主要作用是存储记录,所以“最小和最大记录”和“用户记录”部分占了页结构的主要空间。另外空闲空间是个灵活的部分,当有新的记录插入时,会从空闲空间中进行分配用于存储新记录,如下图所示:
innodbpagestruct21024x415.jpg

一个页内必须存储2行记录,否则就不是B+tree,而是链表了。

第三部分是索引部分,这部分重点指的是页目录(示意图2中的s0-sn),它起到了记录的索引作用,因为在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索,因此在页目录中提供了二分查找的方式,用来提高记录的检索效率。这个过程就好比是给记录创建了一个目录:

将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
1组,也就是最小记录所在的分组只有1个记录;最后一组,就是最大记录所在的分组,会有1-8条记录;其余的组记录数量在4-8条之间。这样做的好处是,除了第1组(最小记录所在组)以外,其余组的记录数会尽量平分。在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。如下图所示:
innodbpagedir1024x959.jpg

页目录存储的是槽,槽相当于分组记录的索引。我们通过槽查找记录,实际上就是在做二分查找。这里我以上面的图示进行举例,5个槽的编号分别为01234,我想查找主键为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_innodbage列为例,age索引的索引结果如下图。

2.3.2 磁盘数据如何加载到InnoDB内存中

2.3.2.1 机械硬盘如何读取数据?

表中的数据是存储在磁盘文件上的,MySQL在处理数据时,需要先把数据从磁盘上读取到内存中。

1.一个硬盘一般由多个盘片组成,盘片的数量一般都在5片以内。

20191123182101621.png

盘片的逻辑结构主要分为磁道、扇区和拄面。一个盘面被分为若干个磁道,每个磁道又被划分为多个扇区。扇区是磁盘存储的最小单位,大小是512字节。

下图显示的是一个盘面,盘面中一圈圈灰色同心圆环为一条条磁道,从圆心向外画直线,可以将磁道划分为若干个弧段,每个磁道上一个弧段被称之为一个扇区(图中绿色部分),每一个盘面有300~1024个磁道。
20191123183720314.png

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。最小单位由操作系统决定,不同的操作系统可能会有所不同。
MySQLInnoDB存储引擎的数据读取以页为单位,也大小由参数innodb_page_size控制,默认值是16k

2.3.1.2 InnoDB内存结构

d829920649c27f35a88dde148795d1f8.jpg

2.4 MySQL是如何实现事务的?

2.4.1 原子性,持久性和一致性

原子性,持久性和一致性主要是通过redo logundo logForce Log at CommitDoubleWrite机制来完成的。

redo log用于在崩溃时恢复数据
undo log用于对事务回滚时进行撤销,也会用于隔离性的多版本控制。
Force Log at Commit机制保证事务提交后redo log日志都已经持久化。
Double Write机制用来提高数据库的可靠性,用来解决脏页落盘时部分写失效问题。

2.4.2 InnoDB事务整体流程分析

20210710000601.png

2.4.3 使用redolog实现事务的一致性和持久性

2.4.3.1 内存数据落盘整体思路分析

169677120201113115748285797315423.png

2.4.3.2 CheckPoint检查点机制

  • 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间。
  • 当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。
  • 当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的。重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需要使用,那么必须强制Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

2.4.3.3 DoubleWrite双写

如果说Insert BufferInnoDB存储引擎带来了性能上的提升,那么Double Write带给InnoDB存储引擎的是数据页的可靠性。
12222222222222222.png

如上图所示,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 logupdate undo log

  • insert undo log:

    是在insert操作中产生的undo log。因为insert操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。

  • update undo log:

    updatedelete操作中产生的undo log
    因为会对已经存在的记录产生影响,为了提供MVCC机制,因此update undo log不能在事务提交时就进行删除,而是将事务提交时放到入history list上,等待purge线程进行最后的删除操作。
    为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undolog的并发写入和持久化。回滚段实际上是一种Undo文件组织方式。

    2.4.4.2 ReadView

对于使用READUNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了。
对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。
对于使用READCOMMITTEDREPEATABLEREAD隔离级别的事务来说,就需要用到我们上边所说的版本链了。
核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。所以设计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中,READCOMMITTEDREPEATABLEREAD隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

2.5 MySQL是如何加行锁的?

2.5.1 RR隔离级别下的加锁机制

133330210710001644.png

2.5.2 RC隔离级别下的加锁机制

间隙锁时为了解决幻读问题,在RC允许出现幻读现象所以RC隔离级别下行锁都加的是记录锁。只有在外键约束检查(foreign-key constraint checking)以及唯一键检查(duplicate-keychecking)时会使用间隙锁封锁区间。

参考文章

  • MySQL体系结构
  • MySQL结构体系
  • MySQL是如何查询一条语句的