MySQL执行过程
MySQL执行过程
MySQL分为客户端(如mysql命令行工具、JDBC驱动、Navicat、SQLyog等)和服务端两部分,应用程序使用客户端和服务器端进行交互。服务端用于接收客户端请求,处理SQL语句并返回结果。
服务端主要包括两个东西,Server 层和存储引擎层。
Server层包括连接器、查询缓存、分析器、预处理器、优化器、执行器、binlog日志模块(用于整个数据库操作记录,主从复制的关键)。
存储引擎层主要负责数据的存储和读取。
以下为MySQL数据库中SQL语句在的简要执行流程:
下面来好好聊聊这个执行过程。
Server层组件
连接器(Connector)
客户端想要对数据库进行操作时,连接器就是用来负责跟客户端建立连接、获取权限、维持和管理连接的。连接器支持短连接也支持长连接。
连接器的功能主要是:
- 管理客户端连接(如 Navicat、SQLyog、应用程序等)。
- 负责身份认证(用户名/密码验证)。
- 获取全局权限。一旦认证成功(账号密码正确),连接器会从权限表中加载该用户的全局权限(例如 GLOBAL 级别的 SELECT, UPDATE, FILE 等权限),并缓存在连接会话中。如果改变权限,需要重新进行连接才会有用。
- MySQL通过线程来处理客户端连接。传统上采用“每连接每线程”(One-Thread-Per-Connection)的模型【注意:MySQL新的版本可能使用更高效的线程池等模型】。为了优化性能,MySQL使用了线程缓存(Thread Cache) 机制:当建立新连接时,MySQL的连接会优先从线程缓存中获取空闲线程;如果缓存中没有,才会创建新的线程。当连接断开时,其线程会被回收到缓存中以供重用,而非立即销毁。线程缓存的大小由参数
thread_cache_size
控制。
扩展:线程缓存池仅减少了每次连接时的线程创建,减少了关闭连接时的线程销毁,但是客户端每次连接仍需经历完整的 TCP 握手和认证【MySQL 协议是基于 TCP 传输层(默认端口 3306)的,必须经过 三次握手 建立连接】。
每次连接都进行握手是比较耗时的过程。解决的方法就是使用druid连接池。
Druid连接池(客户端连接池):每次进行连接的时候,都会从连接池中获取新连接(如果无空闲连接,要进行创建连接去连接数据库,并且进行TCP 握手/认证)。如果是复用连接的话,那么就是不用进行TCP握手,用完就放回连接池。如果没有空闲的连接进行复用,那么就只能去创建连接,然后用完放到连接池里面去。这样的话,就可以减少TCP握手了。
但是注意,MySQL中有一个wait_timeout参数是用于控制MySQL的连接空闲多久就主动断开的,这个值默认是8小时。如果MySQL主动断开,但是druid连接池不知道,那么当应用程序下一次尝试从这个池子里获取这个已经被关闭的连接时,就会抛出
Communications link failure
或Connection reset
之类的异常。为了解决这个问题,有几种解决方案:
- testWhileIdle+validationQuery+timeBetweenEvictionRunsMillis方式:
效果:每过60000毫秒,druid应用就主动去执行一次SELECT 1 FROM DUAL,这样就可以重置MySQL的连接空闲时间了,如果连接检测发现是失败的,druid会丢弃这条无效的连接。相当于让druid主动去检测连接了,并且这种机制“绕过”或“抵消”了
wait_timeout
的效果,就让druid主动去管理连接了。- 在yml中配置配置`testWhileIdle: true`。testWhileIdle设置为 true,意味着 Druid 会对空闲了一段时间的连接进行有效性检查 - 在yml中配置`validationQuery: SELECT 1 FROM DUAL`。设置一个简单的 SQL 语句来验证连接是否有效。 - 在yml中配置`timeBetweenEvictionRunsMillis: 60000`。效果是,让druid间隔多久主动进行一次检测,检测需要关闭的空闲连接,单位是毫秒,建议设置为比 wait_timeout 短得多的时间。
- testOnBorrow+validationQuery
效果是:每次获取连接时,都会先检查一下,如果连接有效,才使用这个连接,如果无效,就换一个连接。但是这样的每次都会额外的网络往返(执行 SELECT 1),会给系统带来微小的性能开销。在高并发场景下,这个开销会更大。所以通常更推荐使用 testWhileIdle+validationQuery+timeBetweenEvictionRunsMillis方式。所以通常设置testOnBorrow:false
- 在yml中配置`testOnBorrow:true`,启用借用检查,这样每次从连接池获取连接时,都会先用 validationQuery 测试一下连接是否有效。 - 在yml中配置`validationQuery: SELECT 1 FROM DUAL`。设置一个简单的 SQL 语句来验证连接是否有效。
MySQL内的线程缓存池和Druid的连接池不一样:
- 位置不一样:线程缓存池是MySQL内的。Druid连接池一般是我们Java应用程序中的
- 目的不一样:Druid的连接池用于管理应用程序与数据库之间的网络连接(Connection)的,为了减少握手的时间。MySQL的线程缓存池用于管理处理客户端连接请求的线程(Thread),为了减少线程创建和销毁的时间。
让我们通过一个请求流程来看Druid连接池和MySQL线程缓冲池是如何配合的:
- 应用请求数据:你的Java程序需要查询用户数据。
- Druid 工作:程序向Druid连接池请求一个连接。Druid从它的池子里找到一个空闲的、已经建立好TCP连接的连接对象给你。
- 网络通信:去连接MySQL。Druid返回的连接对象,其底层的TCP连接通向MySQL服务器的3306端口。
- MySQL 工作:MySQL服务器接收到这个TCP连接请求。
- 线程缓存工作:MySQL检查它的
线程缓存
中是否有空闲的线程。如果有,就拿出来处理这个连接;如果没有,就创建一个新的线程。- 执行查询:这个线程开始工作,解析SQL、执行查询、返回结果。
- 归还连接:你的Java程序拿到结果后,调用
.close()
方法,将连接归还给Druid连接池(注意:这里的“关闭”并不是关闭TCP连接,只是把连接对象状态设为空闲,他还是和MySQL线程缓冲池的某个线程保持连接的。)。- 断开连接:当Druid连接池中的连接长时间空闲时,可能会关闭多余的连接。关闭时,才是断开和MySQL线程缓冲池连接的时候,断开连接时,如果MySQL的线程缓存还没满(
thread_cache_size
未满),就会把这个现在没事干的线程放入线程缓存池中,以备下一个新连接来时复用。如果缓存已满,MySQL就会直接销毁这个线程。注意,只有Druid决定真正关闭一个连接(例如,连接空闲时间超过minEvictableIdleTimeMillis
或池中连接数过多)时,才会发生TCP断开。此时,MySQL服务器端的对应线程才会被释放,并根据thread_cache_size
的设置决定是放入线程缓存还是直接销毁。
查询缓存(Query Cache,MySQL 8.0 已移除)
在MySQL 8.0之前,当你输入一条SQL查询语句后,MySQL会先检查是否开启了查询缓存(query cache)功能。如果开启了,MySQL会在内存中查找是否有相同的SQL语句已经执行过,且缓存了结果。如果有,MySQL会直接返回缓存中的结果,而不需要再进行后续的处理。这样可以提高查询效率,尤其是对于一些静态表或者很少变化的表。注意:5.7以下虽然是有查询缓存但是也是默认关闭的。
功能:
- 缓存
SELECT
查询语句及其结果(Key-Value 形式,Key 是 SQL,Value 是结果)。 - 命中缓存时直接返回结果,避免重复执行。
移除原因:
- 命中率不高,你sql多一个空格或者大写写成小写都不会命中。
- 缓存失效频繁(表数据变化时缓存失效)。
- 作用不大,还增加了维护缓存的成本,所以取消了。
分析器(Parser)【有些地方也叫解析器】
如果查询缓存没有命中,或者没有开启查询缓存,那么MySQL就会进入分析器阶段。分析器的工作主要是对要执行的SQL语句进行解析,最终得到抽象语法树。
分析器主要是两个功能:
- 词法分析。将一长串的SQL字符串“打碎”成一个个最小的、有意义的单元(称为“词法记号”或“Token”)。词法分析就是提取这些关键字(Token),比如 select,提出查询的表,提出字段名,提出查询条件等等。
- 语法分析。根据MySQL的语法规则,检查这些词法分析提取的Token组合成的序列是否符合SQL语法规范。如果符合,就生成一个解析树(Parse Tree) 或抽象语法树(Abstract Syntax Tree, AST)。这个树形结构清晰地表示了SQL语句的层次结构。如果语句不符合语法规则,分析器会报错,并且提示错误的位置。
预处理器 (Preprocessor)
预处理器会校验表名和字段名是否存在。也会把*
转换为表所有的字段。
关键字是在解析器中进行分析的(词法分析),而非关键字是使用这里的预处理器来分析是否正确的,比如表名、字段名都是非关键字,就是在这里检查是否正确的。
优化器 (Optimizer)
查询优化器会找出执行该语句所有可能使用的方案,然后选择一条最优查询路径,即MySQL认为的效率最高的方式,并生成执行计划。比如你一个表中创建了多个索引,优化器会根据IO和CPU成本,选出代价最小的索引进行执行。
可通过SQL语句前添加上 explain 关键字查看执行计划(重点关注type、key、Extra字段);
优化器的作用是:
- 选择最优的执行计划:优化器会根据SQL语句的语义和表的统计信息,比较不同的执行方案,选择一个成本最低的方案来执行。比如,如果表上有多个索引,优化器会根据索引的选择性,选择一个过滤效果最好的索引来使用。
- 生成执行计划:优化器会将选择好的执行方案,转换成一系列的操作步骤,形成一个执行计划(execution plan)。执行计划中包含了各种操作符(operator),比如表扫描(table scan)、索引查找(index lookup)、连接(join)、排序(sort)等。执行计划通常以树状结构表示,树中的每个节点都是一个操作符,树中的叶子节点是数据源(比如表或者索引),树中的非叶子节点是中间结果集。
执行器(Executor)
在优化器生成执行计划后,MySQL就会进入执行器阶段。执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,执行器就会调用API接口,使用存储引擎读取到或者写入数据,然后把执行返回给连接器,连接器返回数据到客户端。
主要负责:
- 权限校验:在真正执行前检查用户是否有权限。执行器会根据连接器缓存的权限信息,检查你是否有执行当前SQL语句的权限。如果没有权限,执行器会报错,并且返回没有权限的错误信息,比如:ERROR 1142 (42000): SELECT command denied to user ‘guest’@‘localhost’ for table ‘employees’
- 调用存储引擎接口:执行器会根据执行计划中的操作步骤,依次调用存储引擎的接口来执行。比如,如果执行计划中有一个全表扫描的操作,执行器就会调用存储引擎的接口,逐行地从表中读取数据,并且判断是否满足查询条件。如果满足,就将数据返回给;如果不满足,就继续读取下一行,直到表中的所有行都被扫描完毕。
Binlog日志模块
Binlog(二进制日志)记录了所有对数据库进行修改的逻辑操作(SQL 语句或行数据变更) ,例如数据插入、更新、删除、表结构修改等。
主要的作用:
- 主从复制:Binlog 是 MySQL 主从复制的核心。主库会将 Binlog 内容发送给从库,从库通过执行 Binlog 中的事件来保持数据同步,实现读写分离。
- 数据恢复:当数据库发生故障或数据丢失时,可以使用Binlog将数据进行恢复。
- 数据审计:Binlog 记录了所有数据变更的详细记录,可用于数据审计和故障排查。
Binlog 有三种记录格式,可通过 binlog_format
参数设置:
- STATEMENT:记录 SQL 语句,日志文件小,但可能导致主从数据不一致。因为只记录 SQL 语句,一条语句可能影响几百万行数据,但日志里也只有一条语句。缺点是:可能导致主从数据不一致,如果 SQL 语句中使用了不确定性函数(non-deterministic),在主库和从库上执行的结果可能不同。典型的不确定性函数:
UUID()
,SYSDATE()
,RAND()
,USER()
,LOAD_FILE()
等。STATEMENT
模式记录的不是客户端发送的“原始SQL”的精确副本,而是经过MySQL服务器解析和重写(Rewrite)后的标准SQL语句。比如:你执行的是下面这个sql:
INSERT INTO `users` (name, age) VALUES ('Alice', 25); -- Add new user
Binlog中记录的SQL:
INSERT INTO `users` (`name`,`age`) VALUES ('Alice',25)
可以看到,多余的空格和注释被去除了。
- ROW:记录每一行数据的变更,更安全可靠,但日志文件较大。由于记录的是行的实际数据,而不是语句,因此可以完美避免不确定性函数带来的问题。主从库的数据内容完全一致。但是日志会很大,因为一条影响大量数据的 SQL(如批量更新、删除)会产生巨大的 Binlog 日志。例如,一个
DELETE FROM big_table
语句在STATEMENT
模式下只有一条记录,但在ROW
模式下会记录big_table
的每一行数据,导致日志量暴增,占用更多磁盘和网络带宽。并且可读性比较差,你看到的是行的数据(显示为 base64 编码的###
),无法直接看到执行的是哪条 SQL 语句,给问题排查带来一定困难。 - MIXED:混合模式,MySQL 会根据 SQL 语句自动选择 STATEMENT 或 ROW 格式,兼顾日志大小和数据一致性。通常情况下,它使用
STATEMENT
来记录,以节省空间。一旦它认为某条 SQL 可能引发数据不一致(例如包含了UUID()
,SYSDATE()
等函数),就会自动切换为ROW
模式来记录这条语句。
Binlog 属于 MySQL 的 Server 层,而 Redo Log 和 Undo Log 则属于存储引擎层。
存储引擎层组件
存储引擎是对底层物理数据执行实际操作的组件,为Server层提供各种操作数据的 API,数据是被存放在内存或者是磁盘中的。
缓冲池(Buffer Pool)
Buffer Pool 是主内存中的一个区域,用于在内存中缓存表数据和索引数据。当需要读取数据时,InnoDB 会先尝试从 Buffer Pool 中获取该数据所在的页。如果不存在(称为“缺页”),则从磁盘读取该页并放入 Buffer Pool。所有的数据修改操作也都在 Buffer Pool 中完成,产生脏页,再由后台线程异步刷盘。
回滚日志(undo log)
MyISAM没有,InnoDB有。undo log记录了数据被修改前的信息,主要用于事务回滚(ROLLBACK)和实现 MVCC(多版本并发控制)。MyISAM 引擎就不支持事务,自然也没有 Undo Log。InnoDB是有Undo Log的。
重做日志(redo log)
MyISAM没有,InnoDB有。redo log记录了数据修改后的信息,主要用于崩溃恢复(Crash Recovery),确保在系统宕机时,已经提交的事务不会丢失,即保证持久性(Durability)。
执行过程分析
面试题:说说MySQL 执行一条查询语句的内部执行过程。
- **连接池获取 (Druid)**应用程序首先从Druid连接池获取一个空闲连接。如果池中没有,Druid会创建一个新的物理连接。
- **连接器 (Connector)**连接请求到达MySQL服务器后,需要由一个线程来处理。MySQL采用 ‘一个连接对应一个线程’ 的模型。首先,服务器的线程管理模块会尝试从线程缓存(Thread Cache)中获取一个空闲线程;如果缓存中没有,则会创建一个新的操作系统线程。成功分配线程后,该线程会调用连接器的功能,开始处理连接请求:首先进行身份认证,验证账号和密码。如果认证成功,连接器会获取该账号的全局权限信息并缓存起来,用于后续的权限判断。至此,连接便成功建立。
- 查询缓存 (Query Cache) (MySQL < **8.0)**MySQL接下来会检查查询缓存。如果这条SQL语句的结果被完整缓存过,则直接返回结果。(MySQL 8.0已移除该功能)
- **解析器 (Parser)**如果未命中缓存,分析器开始工作。它进行词法分析和语法分析,理解SQL语句的结构,确保语法正确。
- **预处理器 (Preprocessor)**解析器主要对
*
进行处理和非关键字进行检验。比如,将 SELECT * 中的*
扩展为具体的所有列名、检查涉及的表和列是否存在、验证列名是否歧义等。 - **优化器 (Optimizer)**优化器对SQL进行优化。它会生成多种执行方案(如选择索引、决定连接顺序),并选择一个它认为成本最低的执行计划。
- **执行器 (Executor)**执行器根据优化器选的计划,调用存储引擎的接口执行查询。在执行前,它会先检查用户对表是否有操作权限(看执行的sql是什么,然后看看连接器放到缓存中的权限信息,看看有没有执行这个sql的权限,比如你只给了查询权限,这里执行的sql是查询,那么就能进行查询。但是如果是执行修改的sql,这里就会报错了)。
- **存储引擎 (Storage Engine)**存储引擎(如InnoDB)负责具体的数据存取操作。它先在内存缓冲池(Buffer Pool)中查找数据,没有则从磁盘读取,并将结果返回给执行器。
- 结果返回执行器将存储引擎返回的结果集进行处理(如计算、排序)后,返回给连接器,连接器返回给客户端。
- 连接归还客户端应用程序拿到结果后,会关闭逻辑连接(即将其归还给Druid连接池)。Druid会将这个物理连接标记为空闲状态并放回池中,等待下一次被复用,而不会立即关闭它。只有当连接空闲时间超过配置的阈值或者连接太多时,Druid的 evictor 线程才会主动断开这条物理连接。此时,MySQL服务器端会收到断开请求,会根据其 thread_cache_size 的配置,决定是否将刚销毁的线程放入线程缓存中以备后续新连接复用,如果缓存没有慢,会把线程放到缓存池里面,如果缓存已满则直接销毁该线程资源。
执行的过程图如下:
存储引擎是可以插拔的设计,所以是可以切换的,下面是MySQL的逻辑架构图:
存储引擎是怎么执行的,在其他笔记中进行整理。