前言
上一篇介绍了 MySQL 默认的 InnoDB 存储引擎是如何存储和组织数据的,这一篇将介绍 MySQL 的逻辑架构,以及分析一条 SQL 语句的具体执行过程。
逻辑架构
MySQL 的架构共分为两层:Server 层和存储引擎层。
- Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、分析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等)都在 Server 层实现。
- 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的。
执行流程
连接器
连接的过程需要先经过 TCP 三次握手,因为 MySQL 是基于 TCP 协议进行传输的。
TCP 网络连接建立成功后,服务端与客户端之间会建立一个 session 会话,紧接着会对登录的用户名和密码进行效验,首先会查询自身的用户表信息,判断输入的用户名是否存在,如果存在则会判断输入的密码是否正确。密码正确后,会从连接池中分配一条空闲线程维护当前客户端的连接;如果没有空闲线程,则会创建一条新的工作线程。之后线程会查询用户所拥有的权限,并对其授权,后续 SQL 执行时,都会先判断是否具备相应的权限。
空闲连接在超过最大空闲时长(wait_timeout)之后,连接器会自动将它断开。
一个处于空闲状态的连接被服务端主动断开后,客户端并不会马上知道,等到客户端在发起下一个请求的时候,才会收到报错。
连接池
Connection Pool,是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请、使用、释放。主要是为了复用线程、管理线程以及限制最大连接数。
当一个客户端尝试与 MySQL 建立连接时,MySQL 内部都会派发一条线程负责处理该客户端接下来的所有工作。
线程的频繁创建和销毁都会耗费大量资源,通过复用线程的方式,不仅能减少开销,还能避免内存溢出等问题。
数据库连接池可以设置最小连接数和最大连接数:
- 最小连接数:是连接池一直保持的数据库连接,如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费。
- 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中。
查询缓存
连接器的工作完成后,客户端会向 MySQL 服务器发送 SQL 语句,MySQL 服务器收到 SQL 语句后,就会分析出 SQL 语句的第一个字段,看看是什么类型的语句。
如果查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句的哈希值,value 为 SQL 语句查询的结果。
如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。
查询缓存往往弊大于利,因为只要有对表的更新,就会导致表上的所有查询缓存被清空。所以,MySQL8.0 版本直接将查询缓存删掉了。
这里说的查询缓存是 server 层的,也就是 MySQL8.0 版本移除的是 server 层的查询缓存,并不是 Innodb 存储引擎中的 buffer poll。
分析SQL
在正式执行 SQL 查询语句之前, MySQL 会先对 SQL 语句做分析,这个工作交由由分析器来完成。分析器可以将输入的 SQL 语句转换为计算机可以理解的形式(语法树,Syntax Tree)。
分析器会做如下两件事情:
-
词法分析:MySQL 会根据输入的字符串识别出关键字出来,构建出 SQL 语法树。
-
语法分析:根据词法分析的结果,语法分析器会根据语法规则,判断输入的 SQL 语句是否满足语法规则。
语法树大致结构如下:
当词法分析和语法分析出错时,分析器会抛出异常。比如语法结构出错、出现了无法识别的字符等。
表或者字段不存在,并不是在分析器里做的,而是在预处理阶段完成。
执行SQL
每条 SQL 语句主要可以分为以下这三个阶段:
- prepare ,预处理阶段;
- optimize ,优化阶段;
- execute ,执行阶段;
预处理器
- 检查 SQL 查询语句中的表或者字段是否存在;
- 将
select *
中的*
符号,扩展为表上的所有字段
优化器
优化器会根据语法树制定多个执行计划,然后确定最优的执行计划。
- 在表里存在多个索引的时候,决定使用哪个索引;
- 在一个语句有多表关联(join)的时候,决定各个表的连接顺序;
执行器
判断用户权限,然后根据执行计划执行 SQL 语句。
Select流程
执行一条查询语句大致过程如下:
- 连接器:建立连接、校验用户身份和权限;
- 查询缓存:如果开启了查询缓存,则需要判断是否命中,如果命中查询缓存则直接返回,否则继续往下执行;
- 分析器:通过词法分析和语法分析构建语法树,并知道这是一条查询语句;
- 执行器:
- 预处理阶段:检查表或字段是否存在;将
select *
中的*
符号扩展为表上的所有列; - 优化器决定要使用的索引等,选择查询成本最小的执行计划;
- 执行器根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端。
- 预处理阶段:检查表或字段是否存在;将
Update流程
更新语句也类似于查询语句,在执行前需要:
- 连接器连接数据库。
- 分析器通过词法分析和语法分析知道这是一条更新语句。
- 优化器决定要使用的索引等。
- 执行器负责具体的执行过程。
执行器更新一条记录的具体流程如下:
- 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id=1 这一行记录:
- 如果 id=1 这一行所在的数据页本来就在内存 Buffer Pool 中,就直接返回给执行器更新;
- 如果记录不在内存 Buffer Pool,将数据页从磁盘读入到内存 Buffer Pool,返回记录给执行器。
- 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
- 如果一样的话就不进行后续更新流程;
- 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
- 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
- InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面(先写到内存中的 redo log buffer 中,再根据刷盘规则写入到 redo log 文件),这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。(WAL 技术)
- 至此,一条记录更新完了。
- 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
- 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):
- prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
- commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
最后
本文介绍了 MySQL 的逻辑架构和执行流程。逻辑架构分为 Server 层和存储引擎层。执行流程主要是在 Server 层完成的,主要由连接器、分析器、优化器、执行器完成。
下一节将介绍 MySQL 的索引。