第一篇文章,聊 MySQL 的基础架构,不谈细节,先看全貌,从高维度理解问题。

从一个最简单的查询语句切入:

mysql> select * from T where ID = 10;

这条语句在 MySQL 内部的执行过程是什么?分析这条语句的执行过程,从整体上了解 MySQL 的组成。

下面是 MySQL 的基本架构示意图:

MySQL 可以分为两大块: Server 层 与 存储引擎

  • Server层:MySQL 的大部分核心服务功能以及所有内置函数,跨存储引擎的功能都在Server层实现 —— 存储过程、触发器、视图等,具体组成如下:
    • 连接器
    • 查询缓存
    • 分析器
    • 优化器
    • 执行器
  • 存储引擎:负责数据的存储提取,架构模式是插件式的,支持多种存储引擎,常见的是下面三种
    • InnoDB
    • MyISAM
    • Memory

5.5.5 版本开始,InnoDB 成为默认存储引擎 create table 指令默认创建的表,如果不额外指定引擎,默认使用的是 InnoDB,可以使用 create table engine=memory 来使用内存引擎创建表。

使用不同引擎的 table,存储数据的方式不同,支持的功能也不同。多个存储引擎共用一个 Server 层。

连接器

执行一条语句,首先需要连接到对应的数据库,这时候连接器是第一个接触的组件。 连接器负责跟客户端建立连接、获取权限、维持和管理连接,连接对应数据库的命令一般这样写:

mysql -h$ip -p$port -u$user -p

可以看到这里 -p 后面并没有紧跟密码,因为会有泄漏的风险,如果在生产服务器,则强烈不建议直接输入密码,而是像上面这样,输入完 -p 后,再输入密码。

连接命令中的 mysql客户端工具,用来跟服务端建立连接。在完成经典的 TCP 握手后,连接服务器开始认证身份,这时候就需要用到输入的 用户名 和 密码。

  • 如果用户名或密码不对,会得到一个 "Access denied for user" 错误,然后客户端执行结束
  • 如果用户名密码认证通过,连接器会到**权限表中查出登录用户拥有的权限。之后这个连接里的权限判断逻辑**,都依赖此时取到的权限。

这意味着如果一个用户成功建立连接,而之后该用户的权限被修改了,并不能影响已经存在的连接的权限,这是一个快照,而只有再新建新的连接时,才会使用新的权限设置

连接完成后,如果没有后续的动作,这个连接将处于空闲状态,可以使用 show processlist 看到这个连接。下面是我本地的查询结果,Command 列显示为 Sleep 的这一行,就代表了一个空闲连接。

image-20200901144205393

客户端如果太长时间没有操作,连接器就会自动断开,具体的时间由 wait_timeout 参数控制,默认值是 8 小时。

当连接被断开后,客户端再次发送请求,就会提示:Lost connection to MySQL Server during query。这时候如果要继续,就需要重新连接然后再执行请求。

数据库中的长连接:连接成功后,如果客户端持续有请求,则一直使用同一个连接。

数据库中的短连接:每次执行完很少的几次查询就断开连接,下次查询再重新建立连接。

由于连接的建立比较复杂,所以尽量使用长连接,减少建立连接的动作。但是全部使用长连接会带来问题:MySQL占用内存增长非常快 —— MySQL 在执行过程中临时使用的内存是被每个连接对象管理,这些资源在连接断开的时候才被释放,如果连接长时间不断开并且连接数量累积很多,则会导致内存占用过大,被系统强制杀掉(OOM),从**表面现象**看就是 MySQL 异常重启

对应的解决方案:

  1. 定期断开长连接:使用一段时间,或者程序判断执行过一个占用大量内存的查询后,断开连接,之后要查询再重连。
  2. 如果使用的 MySQL 版本高于 5.7,可以在每次执行一个占用内存较大的操作后,使用 mysql_reset_connection 重新初始化连接资源。这个过程不需要重连和重新验证权限,但是会将连接恢复到刚创建完的状态。【也就是查询临时内存被释放了。

查询缓存

建立连接之后,就开始执行具体的 select 查询语句,此时执行逻辑会进行到第二步:查询缓存

如果是 MySQL 8.0 之前的版本,MySQL 在拿到一个查询请求后,会先到查询缓存中查找是否执行过相同的语句,之前执行过的语句以及对应的查询结果可能会以 key-value 的形式被直接缓存在内存中。如果能根据语句找到对应的查询结果,则这个 value 会被直接返回给客户端。

但是查询缓存一般不建议被使用,因为一般利大于弊。

查询缓存的弊端:失效非常频繁,只要有对一个表的更新,则这个表上的所有查询缓存都会失效被清空。这就导致很可能耗费很多内存将结果缓存起来,还没使用到就被清空了,尤其是更新频繁的表,基本没用

如果是静态表,更新时间很久,比如系统配置表,才适合用查询缓存。

MySQL 提供了按需使用的方式:将 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句是不使用查询缓存的,需要使用查询缓存时可以手动指定

mysql> select SQL_CACHE * from T where ID = 10;

MySQL 在 8.0 彻底移除了查询缓存这个功能。

分析器

如果查询没有命中缓存,则开始真正执行查询语句。 MySQL 首先对输入的 SQL 语句进行解析

分析器会先做"词法分析",你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。

select 关键字可以识别出,这是一个查询语句,字符串 T 代表着表名 T,字符串"ID" 代表着列 "ID"

**做完词法分析,下一步是语法分析。**根据词法分析的结果,语法分析器会根据语法规则,判断输入的 SQL 语句是否满足 MySQL 语法。

如果语句不满足 MySQL 的语法,则会抛出 "You have an error in your SQL syntax" 错误,比如下面这个语句 select 少打了个 "s":

mysql> elect * from t where ID = 1;

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 * from t where ID = 1' at line 1

一般语法错误会提示第一个错误出现的位置,所以需要关注 use near 后面跟着的内容。

优化器

经过分析器之后,MySQL 已经明白了你的意图,而在真正开始执行之前还要经过优化器的处理。

当要查询的表中存在多个索引时,优化器决定使用哪个索引;或者一个语句关联了多个表(join)时,优化器决定各个表的连接顺序。

比如下面语句:需要执行两个表的 join 操作

mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
  • 既可以先从表 t1 中取出 c=10 的记录的 ID 值,再根据 ID 值关联表 t2,再判断 t2d 的值是否等于 20.
  • 也可以先从 t2 取出 d=20 的记录的 ID 值,再根据 ID 关联到 t1,再判断 t1 里的 c 是否等于10.

上面两种执行逻辑的结果相同,但是执行效率不同,优化器的作用就是决定使用哪种方案。这只是一个最简略的介绍,后面会详细说明优化器的工作内容。

执行器

分析器明确了要做什么,优化器明确了怎么做,下一步就是具体的执行阶段。

具体执行语句的时候,先判断当前连接是否对表 T 具有查询权限,如果没有,则返回一个没有权限的错误,如下所示(在工程实现中,如果命中查询缓存,会在查询缓存返回结果的时候做权限验证。查询会在优化器之前调用 precheck 验证权限):

mysql> sekect * from T where ID = 10;
ERROR 1142(42000):SELECT command denied to user 'b'@localhost for table 'T'

如果权限正确,则打开对应表继续执行。打开表时执行器根据表的引擎定义,使用对应引擎提供的接口

比如例子中的表 TID 字段没有索引,则执行流程如下:

  1. 调用 InnoDB 引擎接口获取表的第一行,判断 ID 值是否为10,如果不是则跳过,是则将这行保存在结果集中。
  2. 调用引擎获取下一行,使用相同的判断逻辑,直到表的最后一行。
  3. 执行器将完成上述遍历后的满足条件的行组成记录集作为结果集返回给客户端。

此时,这条语句就执行完成。

如果查询的表中存在索引,执行逻辑也差不多。第一次调用的是"取满足条件的第一行" 这个接口,之后循环取"满足条件的下一行" 这个接口,这些接口查询引擎已经定义好的

数据库慢查询中有一个 rows_examined 字段,表示这个语句执行过程扫描了多少行,这个值是在执行器每次调用引擎获取数据行的时候进行累加

小结

本章简单介绍了一个查询语句在 MySQL 中的执行过程以及 MySQL 的组成部分与各部分的具体功能。作者虽然讲的比较简略,但是核心还是点到了,由每个部分可以扩展出的知识非常多,期待后续的课程。

个人总结

MySQL的架构分为两层:Server层 和 存储引擎。其中存储引擎有自己的特性,而 Server层的架构是不变的,也就是多个存储引擎对应一个 MySQL Server。

本篇文章只介绍了 MySQL Server 层的架构与各个组件的作用,并没有涉及到存储引擎部分的知识。

  • Server
    • 连接器:与客户端建立连接、获取权限、维持和管理连接。
    • 查询缓存:查询结果会以 key-value 的形式缓存在内存中,如果根据当前查询语句找到了对应的查询结果,则直接返回,但是对表的更新会使查询缓存失效,所以实际上很难命中,除非数据是静态的,例如系统配置。 MySQL 8.0 移除了查询缓存
    • 分析器:对客户端输入的 SQL 语句进行 「语法分析」和「词法分析」,如果分析结果不符合 MySQL 语法,就会报错。
    • 优化器:类似 Java 的编译器对源代码会进行一部分优化,MySQL 优化器决定了在表中存在多个索引,或存在多个表关联查询的时候各个表的连接顺序,这些语句的执行结果是一样的,但是不通的执行顺序的执行效率是不一样的,优化器确定最高效率的执行方案。
    • 执行器:真正开始执行这条 SQL 语句,调用存储引擎的接口,与「存储引擎」进行交互。
  • 存储引擎

课后问题:

如果表 T 中没有字段 k,而你输入了语句

select * from T where k = 1;

则一定会抛出 不存在这个列 错误,这个错误是在哪个阶段抛出的呢?

A:

在**分析器执行阶段错误被抛出,分析器在优化之前会分析词法语法是否正确,以及查询的列和表是否存在**。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。