01_MySQL基础架构
MySQL 45 讲Note:
课程专栏名称:《MySQL实战45讲》课程
笔记参考:MYSQL45 讲
01_基础架构:一条SQL查询语句是如何执行的?
一条SQL查询是如何执行的
先看一下下面这个图
我们首先理解一下 Mysql 的基础架构,理解如果执行一条简单的查询语句,Mysql 进行了哪些操作。
在 MySql 的基础架构种,他分为了Service 层和存储引擎;
其中存储引擎负责存储和提取数据,Service 层包含了连接器,查询缓存,优化器和执行层等,蕴含了Mysql 大多数的核心功能。
接下来我们先来了解一下他们的基础概念。
存储引擎
Mysql 常见的存储引擎有 InnoDB、MyISAM、Memory 等多种,最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
连接器
我们使用客户端和 Mysql 进行连接的时候,Mysql 连接器就负责管理连接,建立连接,获取权限,维持连接
具体的一个连接操作:
连接命令中的 mysql 是客户端工具,用来跟服务端建立连接。在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。
- 如果用户名或密码不对,你就会收到一个”Access denied for user”的错误,然后客户端程序结束执行。
- 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接****。
但如果全部使用长连接的话,会出现一个问题:
MySQL 建立连接的时候,每个客户端连接都会有一个对应的连接对象(Connection Object),这个连接对象会维护连接过程中的一些状态信息,比如事务状态、锁信息、临时表等。同时,连接对象也会维护一些缓存信息,比如查询结果缓存、语句缓存等。这些缓存信息会占用一定的内存空间。
当MySQL执行查询语句时,会为查询分配一些内存空间,用于存储临时表、排序缓存、哈希表等中间结果。这些内存空间是从连接对象中分配的,因此被称为连接内存(Connection Memory)。这些内存空间只有在连接断开的时候才会被释放,因为它们是绑定在连接对象上的,只有当连接对象被销毁时,这些内存空间才会被系统回收。
如果使用长连接,那么连接对象会一直存在,连接内存也就会一直被占用。如果多个长连接同时存在,那么这些连接对象和连接内存就会累积,导致MySQL占用的内存空间越来越大。因此,长连接也需要注意内存占用问题,需要在代码中合理管理连接对象和连接内存的生命周期,避免内存泄漏和OOM问题的发生。
所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。
怎么解决这个问题呢?你可以考虑以下两种方案。
- 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连****。
- 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
查询缓存
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。
之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。
如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。
但是大多数情况下建议不要使用查询缓存,查询缓存往往弊大于利。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。
查询缓存是以查询语句作为 key,如果查询的数据发生了变化,那么查询语句所对应的结果也会发生变化,即使查询语句不变。
因此,当数据发生变化时,MySQL会自动使查询缓存失效,下次查询时会重新执行查询语句并缓存新的结果。这也是为什么有时候查询缓存机制反而会降低性能的原因,因为每次数据发生变化时都需要重新查询并缓存结果,而且查询缓存本身也会占用一定的内存空间。
除非你的业务就是有一张静态表,很长时间才会更新一次。
比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
MySQL 也提供了这种“按需使用”的方式。可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。
而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from T where ID=10;
需要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。
分析器
如果没有命中查询缓存,就要开始真正执行语句了。首先语句经历的第一步就是这个分析器。
对SQL语句进行解析,Mysql 才能知道你要做什么。首先分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
MySQL 从你输入的”select”这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 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,再判断 t2 里面 d 的值是否等于 20。
- 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
执行器
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
执行器的主要作用是将SQL语句转换为操作存储引擎的指令,并将结果返回给客户端。
在执行器中,会根据SQL语句的类型(SELECT、INSERT、UPDATE、DELETE等)和表的引擎类型,调用相应的存储引擎接口来执行操作。
同时,在执行查询SQL的时候,会先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误(在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
precheck 验证权限 和 执行器验证权限的区别:
- 预验证是在执行查询之前进行的,主要是为了避免无效查询的开销。在预验证中,MySQL会检查当前用户是否具有执行该查询的权限,如果没有,就可以直接返回没有权限的错误,避免了执行查询的开销。预验证只是简单地检查当前用户是否具有执行该查询的权限,不会涉及到表的引擎类型、表的结构等因素。
- 执行器中的权限验证是在执行查询时进行的,主要是为了确保操作的合法性。在执行器中,MySQL会根据SQL语句的类型和表的引擎类型,调用相应的存储引擎接口来执行操作。在执行操作之前,MySQL会进行权限验证,检查当前用户是否具有执行该操作的权限,以及表的引擎类型、表的结构等因素是否符合要求。这样可以确保操作的合法性,避免了恶意操作或者误操作。
- 因此,预验证和执行器中的权限验证虽然都是为了验证当前用户是否具有执行查询的权限,但它们的目的和方式是不同的。预验证主要是为了避免无效查询的开销,而执行器中的权限验证主要是为了确保操作的合法性。