一、MySQL整体架构
MySQL的架构主要分为两层:Server层 和存储引擎层。
-
Server层 :负责连接管理、SQL解析、优化以及执行等核心流程。当客户端发起连接时,连接器 负责建立连接、验证身份与权限。同时,内部的连接池/线程管理模块会为每个客户端请求分配一个线程,避免了频繁创建和销毁线程的开销。
-
存储引擎层:负责数据的实际存储与提取。MySQL采用插件式的存储引擎架构,其中最常用的是InnoDB。
1.1 一条SQL查询语句的执行流程
下面,我们以一条查询语句为例,详细拆解它在MySQL内部经历的各个阶段。
第一步:连接器
客户端与MySQL建立连接,始于连接器。
-
建立TCP连接 :MySQL基于TCP协议传输,因此第一步是完成经典的TCP三次握手。
-
身份验证 :连接器验证你提交的用户名和密码。如果验证失败,客户端会收到
Access denied for user的错误信息,并结束执行。 -
获取权限 :验证成功后,连接器会立即从系统权限表中读取该用户的所有权限,并缓存在当前连接会话中。这里有一个关键点:后续此连接中的所有操作,其权限判断都基于此次读取的快照。这意味着,即使连接建立后管理员修改了你的权限,当前已建立的连接也不会生效,必须重新连接。
关于连接管理的几个问题:
-
空闲连接会一直占用资源吗?
不会。MySQL通过
wait_timeout参数控制空闲连接的最大时长,默认是8小时。如果空闲连接超时,连接器会自动断开它。你也可以手动执行kill connection命令来断开。 -
断开连接后客户端会立刻知道吗?
不会。连接断开是服务端单方面操作的,客户端要等到下一次发送请求时,才会收到类似
Lost connection to MySQL server during query的报错。 -
MySQL的连接数有限制吗?
有限制。由
max_connections参数控制,该参数可以手动调整。我的MySQL服务默认是151个。一旦连接数超过这个限制,系统会拒绝后续连接请求,并返回Too many connections错误。
长连接 vs 短连接
MySQL的连接也分为短连接和长连接。由于长连接可以减少建立和断开连接的开销,所以一般推荐使用长连接。
但长连接也有隐患。MySQL在执行操作时会临时使用内存来管理连接对象,这些资源只有在连接断开时才会彻底释放。如果长连接累积过多且执行了大量大操作,会导致MySQL内存占用飙升,严重时可能被操作系统强制杀掉,导致MySQL服务异常重启。
如何解决长连接占用内存的问题?
-
定期断开长连接:在使用完一个批次的操作后,主动断开连接,释放内存。
-
重置连接(MySQL 5.7+) :MySQL 5.7版本提供了
mysql_reset_connection()接口函数。在代码中调用此函数,可以在不重新建立TCP连接的情况下,重置连接状态并释放临时内存,恢复到刚刚建立连接时的状态。
第二步:查询缓存(已废弃)
连接建立后,客户端就可以向MySQL发送SQL语句了。MySQL收到SQL后,首先会解析出语句的第一个字段,判断其类型。
如果是SELECT查询语句,MySQL之前会先去查询缓存中查找。查询缓存以Key-Value形式保存在内存中,Key是SQL查询语句,Value是该语句的查询结果。如果命中缓存,结果会直接返回给客户端;如果未命中,则继续执行后续流程,并在执行结束后将结果存入查询缓存。
为什么从MySQL 8.0开始,查询缓存被移除了?
因为它的弊端非常明显。对一个表的任何更新操作(增、删、改),都会导致该表所有相关的查询缓存被全部清空。如果缓存了一个结果集很大的数据,还没等被使用就被清空,就完全是做无用功了。对于更新频繁的表,缓存命中率极低,维护缓存的成本反而更高。因此,从MySQL 8.0开始,查询缓存功能已被彻底移除,这条路径也就走不通了。
第三步:解析器
在正式执行SQL之前,MySQL必须"读懂"这条语句。这个工作由解析器完成,主要包含两步:
-
词法分析 :将输入的SQL字符串识别出一个个"Token"(关键词、标识符等)。例如,
SELECT username FROM userinfo会被解析出SELECT、username、FROM、userinfo四个Token,其中SELECT和FROM是关键词。 -
语法分析 :根据词法分析的结果,语法解析器会检查SQL语句是否符合MySQL的语法规则。如果没问题,就会构建一棵SQL语法树,方便后续模块获取SQL类型、表名、字段名、WHERE条件等信息。
注意 :如果输入的SQL语句存在语法错误,例如写错了关键词,就会在这个阶段报错。但是,表或字段是否存在,要等到下一阶段------预处理器,才会检测。
第四步:执行SQL
执行SQL语句是整个流程的核心,可以细分为三个阶段:预处理 、优化 和执行。
1. 预处理器
-
检查语义:检查SQL查询语句中涉及的表和字段,判断它们是否真实存在。
-
扩展通配符 :将
SELECT *中的*,扩展为表中所有列的名称。
2. 优化器
优化器的职责是确定一个最高效的执行方案。例如,当表中存在多个索引时,优化器会基于查询成本(如CPU、I/O成本)来决定使用哪个索引。
我们可以通过在查询语句前加上EXPLAIN命令,来查看优化器选择的执行计划。执行计划中的key字段,就表示实际使用到的索引。如果key为NULL,则说明没有使用索引,将会进行全表扫描。
当一条查询既可以使用主键索引,也可以使用二级索引时,优化器会综合评估,选择它认为成本更低的那个方案。
3. 执行器
优化器确定了执行方案后,MySQL就真正开始执行语句了。这个"实干"环节由执行器负责。
执行器的核心工作是与存储引擎进行交互 ,并且这种交互是以记录(Row)为单位的。执行器会根据执行计划,不断调用存储引擎提供的接口,逐行获取数据,并进行条件判断和结果收集。
索引下推
在了解具体的交互过程之前,有必要先介绍一个重要的执行优化手段------索引下推。
索引下推是 MySQL 5.6 引入的一项优化,核心思想是:将部分 WHERE 条件的判断下推到存储引擎层进行过滤,从而减少 Server 层与存储引擎之间的交互次数。
我们通过一个例子来理解。
假设有一张用户表 user,建有一个联合索引 (age, city):
sql
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
city VARCHAR(50),
KEY idx_age_city (age, city)
);
现在执行一条查询:
sql
SELECT * FROM user WHERE age BETWEEN 18 AND 25 AND city = '北京';
由于联合索引是 (age, city),而条件中的 city 是索引的第二列,根据最左前缀原则,索引只能匹配到 age BETWEEN 18 AND 25 这个范围,city = '北京' 无法在索引树中直接过滤。
在索引下推关闭的情况下:
-
存储引擎根据
age BETWEEN 18 AND 25找到所有符合条件的索引记录。 -
每找到一条,就进行一次回表,获取完整的行数据,返回给执行器。
-
执行器拿到数据后,再判断
city = '北京'是否满足,不满足则丢弃。
这意味着,即使最终只有 100 条数据满足 city = '北京',但如果 age BETWEEN 18 AND 25 命中了 10000 条记录,就会发生 10000 次回表,其中 9900 次都是浪费的。
在索引下推开启的情况下(默认开启):
-
存储引擎根据
age BETWEEN 18 AND 25找到索引记录后,直接在存储引擎层 判断city = '北京'是否满足。 -
只有满足
city = '北京'的索引记录,才会进行回表,获取完整的行数据,返回给执行器。
这样一来,回表次数从 10000 次降到了 100 次,大大减少了 I/O 开销。
可以用 EXPLAIN 查看执行计划,如果 Extra 列中出现了 Using index condition,就表示本次查询使用了索引下推。
了解索引下推之后,我们再来看三种具体场景下执行器与存储引擎的交互过程。
(1)主键索引查询
假设执行语句:SELECT * FROM user WHERE id = 10;
-
执行器调用存储引擎的接口,请求读取表中第一行数据。
-
存储引擎根据主键索引(B+树),快速定位到
id = 10的数据页,并返回该行记录。 -
执行器接收到该行数据后,判断其是否满足
WHERE条件(此处为id = 10)。由于主键查询最多只返回一条记录,执行器在拿到数据后,立即将该行加入结果集。 -
执行器再次请求下一行,存储引擎返回"已无更多记录"的信号,执行器结束查询,并将结果集返回给客户端。
(2)普通索引查询(回表)
假设执行语句:SELECT * FROM user WHERE age = 20;(age 字段上建有普通索引)
-
执行器请求存储引擎,根据普通索引
idx_age,读取满足age = 20的第一条记录。 -
存储引擎在
idx_age索引树上找到该记录,获取到对应的主键 ID (例如id = 5)。 -
存储引擎再根据这个主键 ID,回表 到主键索引树进行查找,得到完整的行数据(
id = 5, name = '张三', age = 20),并返回给执行器。 -
执行器判断该行是否满足所有条件(此处为
age = 20),满足则加入结果集。 -
执行器继续请求下一条满足
age = 20的记录,存储引擎重复上述回表过程,直到所有符合条件的记录被读取完毕。 -
执行器将最终结果集返回给客户端。
(3)全表扫描(无索引)
假设执行语句:SELECT * FROM user WHERE age = 20;(但 age 字段没有索引)
-
执行器请求存储引擎,读取表中的第一行数据。
-
存储引擎从数据文件的第一页开始,读取第一行记录,返回给执行器。
-
执行器判断该行的
age字段是否等于20。如果满足,则加入结果集;如果不满足,则丢弃。 -
执行器接着请求下一行,存储引擎继续顺序读取下一行记录。
-
这个过程会一直循环,直到存储引擎将整个表的数据全部扫描完毕,并返回"已无更多记录"的信号。
-
执行器将所有满足条件的行组装成结果集,返回给客户端。