MySQL架构与SQL执行完全解析

前言

当我们向MySQL发送一条SQL语句时,无论是简单的SELECT还是复杂的UPDATE,背后都有一套精密的执行流程。这套流程涉及客户端Server层存储引擎层 的协作,以及Buffer PoolUndo LogRedo LogBinlog等组件的配合。本文将从整体架构出发,深入剖析SQL语句(查询与修改)的完整底层执行逻辑,帮助你对MySQL有一个全局且深入的理解。

一、MySQL整体架构

MySQL采用分层架构 ,核心分为Server层存储引擎层

Server层

负责处理SQL语句的解析、优化、执行,以及权限管理、内置函数、存储过程等跨存储引擎 的功能。Server层有自己的日志------Binlog(归档日志)。

存储引擎层

负责数据的存储与检索 ,采用插件式 架构。最常用的是InnoDB (MySQL 5.5.5开始成为默认引擎),它支持事务、行级锁、外键,并提供Redo Log (重做日志)和Undo Log(撤销日志)来保证事务的ACID特性。

二、SQL完整执行流程图

下面是一张从客户端请求到结果返回 的完整流程图,它整合了查询和更新(特别是事务性更新)的所有关键步骤。对于查询语句,会跳过日志写入和两阶段提交部分。

说明 :对于SELECT查询,上述流程中从写入Undo LogRedo Log commit的步骤不会发生,执行器直接通过引擎接口读取数据并返回。

三、各步骤详细解说

1. 连接器

职责:管理客户端连接、验证身份、获取权限。

  • 客户端通过mysql -h$ip -P$port -u$user -p命令发起连接,连接器完成TCP握手后认证用户名和密码。
  • 认证通过后,连接器会从权限表读取该用户拥有的权限,此后的所有操作都基于本次读取的权限(即使中途管理员修改了权限,也不影响已存在的连接)。
  • 连接完成后,如果客户端长时间没有动作,连接器会自动断开,由参数wait_timeout控制(默认8小时)。断开后再次发送请求会收到"Lost connection"错误。

明白了,需要在不破坏原有结构 的前提下,在"长连接 vs 短连接"及"长连接内存问题及解决"之后,新增一个小节 专门讲解连接池,并配图。我会保持你原有文字的完整性,新增内容紧跟在解决方案之后。

长连接 vs 短连接
  • 长连接:连接成功后持续使用同一连接。建议使用,避免频繁建立连接的开销。
  • 短连接:每次执行少量查询后断开,下次重建。不推荐。
长连接内存问题及解决

MySQL执行过程中临时使用的内存(如结果集、排序缓存)管理在连接对象中,只有连接断开才会释放。长连接累积可能导致内存暴涨,引发OOM或被系统杀掉。

解决方案

  • 定期断开长连接,释放内存。
  • MySQL 5.7+ 可使用mysql_reset_connection()重新初始化连接资源(无需重连和权限验证)。
连接池(Connection Pool)

即使我们使用了长连接,每次新建连接仍然需要经历TCP握手、MySQL认证、权限读取等开销,在高并发场景下依然可能成为瓶颈。连接池是一种更高效的连接管理方案。

连接池的核心思想 :在应用服务器端预先创建并维护一组"已经建立好的数据库连接",当应用程序需要访问数据库时,直接从池中借用 一个空闲连接,使用完毕后归还到池中,而不是真正关闭连接。

连接池的优势

  • 减少连接建立开销:避免了频繁的TCP握手、MySQL认证、权限查询等耗时操作。
  • 限制连接数量:可以设置最大连接数,防止过多连接压垮数据库。
  • 统一管理连接生命周期:支持超时回收、连接有效性检测、自动重连等。
  • 提升系统响应速度:请求到来时无需等待连接建立。

连接池的常见参数(以HikariCP、Druid为例):

  • maximumPoolSize:池中最大连接数
  • minimumIdle:池中保持的最小空闲连接数
  • connectionTimeout:获取连接的超时时间
  • idleTimeout:空闲连接的最大存活时间

连接池工作流程示意
MySQL 服务器
数据库连接池
应用程序
借用连接
借用连接
执行SQL
执行SQL
归还连接
变为空闲
线程1
线程2
线程3
连接1

空闲
连接2

使用中
连接3

空闲
连接4

空闲
接收SQL执行

为什么连接池能缓解长连接内存问题?

  • 连接池通常会设置idleTimeout,空闲连接超过一定时间会被主动关闭并移除,从而释放连接所占用的MySQL内存(临时结果集、排序缓存等)。
  • 同时,连接池可以定期执行mysql_reset_connection()(如Druid的reset配置)来重置连接状态,避免内存累积。

注意 :连接池是应用层 的机制,而MySQL自身的wait_timeoutmysql_reset_connection()数据库层的机制。两者配合使用,可以更好地管理长连接资源。

2. 查询缓存(MySQL 8.0 已移除)

在MySQL 5.7及之前版本,连接完成后会先查询缓存。缓存以key-value形式存储,key是SQL语句,value是查询结果。

为什么不建议使用?

  • 命中率低:只要表有更新(INSERT、UPDATE、DELETE),该表的所有查询缓存立即失效。
  • 维护成本高:对更新频繁的表,缓存频繁失效,反而增加系统负担。
  • MySQL 8.0 直接移除:官方认为其弊大于利。

按需使用方式(针对旧版本):

  • 设置query_cache_type = DEMAND,默认不缓存。
  • 需要缓存的查询使用SELECT SQL_CACHE * FROM ...

3. 解析器

作用:将SQL字符串解析成MySQL能理解的结构。

  • 词法分析 :将SQL拆分成token,识别关键字(SELECT、UPDATE)、表名、列名、操作符等。
  • 语法分析 :根据MySQL语法规则检查SQL结构是否正确。例如SELECT * FORM user会报错,提示near 'FORM'

如果语法错误,MySQL会返回ERROR 1064 (42000): You have an error in your SQL syntax,并指出第一个出错位置。

4. 预处理器

预处理器在解析器之后执行,负责语义检查视图展开

  • 检查表名、列名是否存在。
  • 检查用户是否有访问这些对象的权限(部分权限检查在此阶段进行)。
  • 检查数据类型是否匹配。
  • 如果SQL中使用了视图,将其展开为实际的基础表操作。

5. 优化器

职责 :基于解析树生成多个执行计划,并选择代价最小的一个。

优化器决策内容包括:

  • 选择使用哪个索引(或者全表扫描)。
  • 多表连接时的连接顺序 (例如t1 join t2,先扫描t1还是t2)。
  • 子查询优化、条件简化等。

示例

sql 复制代码
SELECT * FROM t1 JOIN t2 USING(id) WHERE t1.c=10 AND t2.d=20;

两种执行方案:

  1. 先从t1取c=10的id,再关联t2,再判断d=20。
  2. 先从t2取d=20的id,再关联t1,再判断c=10。

优化器会根据表统计信息(如数据量、索引区分度)选择效率更高的方案。

6. 执行器

职责:按照优化器的执行计划,调用存储引擎接口来操作数据。

  • 首先检查用户对目标表是否有执行权限(有些权限在预处理阶段已检查,但触发器这类运行时对象只能在此检查)。
  • 打开表,根据存储引擎定义,循环调用引擎接口:
    • 对于查询:调用引擎取第一行,判断是否符合WHERE条件,符合则放入结果集,否则跳过;然后取下一行,直到最后。
    • 对于更新:调用引擎取到行数据,修改后调用引擎写入接口。
  • 最后将结果集返回客户端。

如果启用了查询缓存且命中,权限验证会在缓存返回结果时进行。

四、存储引擎层(InnoDB)深入解析

当SQL是更新操作(INSERT/UPDATE/DELETE)时,执行器会调用InnoDB引擎接口,触发如下完整事务流程。如果是查询操作,则直接读取数据返回,不涉及日志写入。

4.1 读取数据到Buffer Pool

  • Buffer Pool:InnoDB的内存缓冲池,以**页(Page)**为单位存储数据副本(默认16KB)。
  • 执行器请求某行数据时,InnoDB先检查所需数据页是否在Buffer Pool中:
    • 如果在(命中),直接使用内存中的数据。
    • 如果不在,从磁盘读取数据页到Buffer Pool。

4.2 写入Undo Log

  • Undo Log :记录数据修改之前的旧值,存储在Undo表空间。
  • 作用
    • 事务回滚:如果事务需要回滚,利用Undo Log将数据恢复原状。
    • MVCC(多版本并发控制):为读操作提供数据的旧版本,实现可重复读等隔离级别。
  • 例如:UPDATE users SET balance = balance - 100 WHERE id = 1,Undo Log会记录id=1这行原来的balance值。

4.3 更新Buffer Pool中的数据

  • 在Buffer Pool中直接修改数据页,此时内存中的数据和磁盘不一致 ,该页变为脏页
  • 为提高性能,修改不会立即写回磁盘,而是延迟到后台线程统一刷盘。

4.4 写入Redo Log Buffer

  • Redo Log:物理日志,记录"在某个表空间的某个数据页的某个偏移量处做了什么修改",例如"在页100的偏移量200处写入值x"。
  • 修改操作会先在内存的Redo Log Buffer中写入Redo日志,而不是直接写文件。
  • WAL技术(Write-Ahead Logging):先写日志,后写磁盘,保证crash-safe。

4.5 Redo Log刷盘(Prepare阶段)

  • 当事务提交时,Redo Log Buffer的内容会写入磁盘上的Redo Log文件(一组固定大小的文件,循环使用)。
  • 刷盘时机由参数innodb_flush_log_at_trx_commit控制:
    • 0:每秒写入一次,最不安全(可能丢失1秒事务)。
    • 1:每次事务提交都刷盘,最安全(默认值)。
    • 2:每次事务提交写入OS缓存,每秒刷盘,性能与安全折中。
  • 此时Redo Log的状态标记为prepare(两阶段提交的第一步)。

4.6 写入Binlog

  • Binlog :Server层的逻辑日志,记录SQL语句的原始逻辑(如"给id=1的balance减100"),以追加写方式记录,不会覆盖旧日志。
  • 作用
    • 主从复制:从库重放Binlog实现数据同步。
    • 数据恢复:基于全量备份+Binlog增量恢复到任意时间点。
  • 执行器生成Binlog并写入磁盘。

4.7 Redo Log标记Commit

  • 当Binlog写入成功后,InnoDB会将Redo Log中对应的事务状态从prepare 改为commit
  • 这就是两阶段提交(2PC)的完成。目的是保证Redo Log和Binlog的逻辑一致性。

4.8 后台IO线程异步刷脏页

  • 脏页:Buffer Pool中被修改但尚未写回磁盘的数据页。
  • 后台线程会在以下时机将脏页刷回磁盘:
    • Buffer Pool空间不足。
    • 脏页比例超过阈值。
    • 后台线程定期刷新。
    • MySQL空闲时。
  • Checkpoint机制标记哪些脏页已刷盘,以便崩溃恢复时知道从哪个Redo Log位置开始重做。

4.9 返回结果给客户端

  • 执行器收到存储引擎提交完成的信号后,向客户端返回Query OK, 1 row affected之类的执行结果。

你说得对,第一份文档(PDF)中对三大核心日志的讲解非常详尽,包括粉板比喻、WAL技术、两阶段提交、崩溃恢复规则等。我之前在"五、三大核心日志对比"部分确实过于简略了。现在我把这个章节按照第一份文档的详细程度进行完整扩展,保持与前面内容的风格一致。

以下是替换后的 第五章完整内容(你可以直接替换原博客中的对应章节):

五、三大核心日志详解

在MySQL(InnoDB引擎)中,Redo LogUndo LogBinlog 共同保证了事务的ACID特性以及数据恢复、主从复制的能力。理解这三种日志的工作机制,是掌握MySQL内核的关键。

5.1 Redo Log(重做日志)------ 保证持久性

为什么需要Redo Log?

想象一下,如果没有Redo Log,每一次更新操作都要直接写入磁盘:找到对应的数据页、修改、写回。这个过程涉及大量的随机I/O ,性能极差。MySQL的设计者借鉴了"酒店掌柜的粉板"思路,使用WAL技术(Write-Ahead Logging,预写日志)来提升效率。

孔乙己的粉板比喻 :酒店掌柜有一个粉板(白漆木板)用来记录客人赊账,如果赊账人不多,可以直接翻账本记账;但如果生意红火,掌柜会先在粉板上记一笔,等打烊后再把账本翻出来核对入账。粉板写得快,账本查得慢。MySQL也是如此:先写日志(粉板),后写磁盘(账本)

Redo Log的工作原理
  • 记录内容:物理日志,记录"在某个数据页的某个偏移量做了什么修改"。例如:"在表空间X的页面Y的偏移量Z处写入值W"。
  • 存储位置 :磁盘上一组固定大小的文件(如ib_logfile0ib_logfile1),循环写入,写满后覆盖旧的日志。
  • 写入流程
    1. 更新操作发生时,先写入内存中的 Redo Log Buffer
    2. 事务提交时(或按配置策略),将Redo Log Buffer刷入磁盘Redo Log文件。
    3. 后台线程在系统空闲或Checkpoint时将Buffer Pool中的脏页真正刷回数据文件。

下图展示了Redo Log的循环写入结构:

Redo Log的刷盘策略

参数 innodb_flush_log_at_trx_commit 控制刷盘时机:

含义 风险 性能
0 每秒写入一次Redo Log文件 崩溃可能丢失1秒事务 最高
1(默认) 每次事务提交都刷盘 不丢失(最安全) 较低
2 每次提交写入OS缓存,每秒刷盘 崩溃可能丢失1秒事务 较高
Redo Log如何实现crash-safe?

数据库异常重启后,InnoDB会扫描Redo Log,重放 所有已提交但尚未写入数据文件的操作,保证数据不丢失。这就是持久性的保障。

5.2 Binlog(归档日志)------ 主从复制与时间点恢复

Binlog是Server层 的日志,所有存储引擎都能使用。它记录的是逻辑修改,而非物理修改。

Binlog的特点
  • 记录内容:逻辑日志,可以是SQL语句(STATEMENT格式)或行变更(ROW格式)或混合格式(MIXED)。例如:"给ID=2这行的c字段加1"。
  • 写入方式追加写 ,文件写满后切换到下一个,永远不会覆盖旧的日志。
  • 主要用途
    • 主从复制:从库重放Binlog实现数据同步。
    • 时间点恢复:基于全量备份 + Binlog增量恢复到任意时刻。
Binlog与Redo Log的核心区别
对比项 Redo Log Binlog
所属层 InnoDB引擎层 Server层
日志类型 物理日志(页修改) 逻辑日志(SQL/行变更)
写入方式 循环写,固定大小 追加写,可多个文件
主要作用 crash-safe,持久性 主从复制,归档恢复
是否支持事务 是(InnoDB特有) 是(所有引擎)
如何用Binlog恢复误删数据?

假如某天中午12点误删了一张表,你可以在备份恢复时这样做:

  1. 找到最近的一次全量备份(比如昨晚0点的备份),恢复到临时库。
  2. 从备份时间点开始,将Binlog依次取出来,重放到中午误删表之前的那个时刻。
  3. 此时临时库就是误删前的状态,从中提取表数据,恢复到线上库。

5.3 Undo Log(撤销日志)------ 事务回滚与MVCC

Undo Log的作用
  • 事务回滚 :当事务执行失败或主动ROLLBACK时,根据Undo Log中记录的旧值 将数据恢复原状,保证原子性
  • MVCC(多版本并发控制) :为读操作提供数据的历史版本,实现可重复读等隔离级别,避免"读-写"冲突。
Undo Log的工作原理
  • 记录内容 :逻辑日志,记录数据修改之前 的值。例如:UPDATE users SET balance=100 WHERE id=1,Undo Log会记录id=1这行原本的balance值。
  • 存储位置 :存储在Undo表空间(通常是独立的文件或共享表空间)。
  • 生命周期
    • 事务开始时,每次修改前先写入Undo Log。
    • 事务提交后,Undo Log不会立即删除,因为可能有其他事务正在读取该旧版本(MVCC)。
    • 当没有任何事务需要这些旧版本时,purge线程会清理过期的Undo Log。
Undo Log与Redo Log的关系
  • Undo Log记录"如何撤销修改"(即旧值),用于回滚和MVCC。
  • Redo Log记录"如何重做修改"(即新值),用于崩溃恢复。
  • 两者在InnoDB内部协调工作:事务提交时,先写Redo Log,再写Binlog(两阶段提交),而Undo Log在修改前就已写入。

5.4 两阶段提交(2PC)------ 保证Redo Log与Binlog一致

为什么需要两阶段提交?

如果不使用两阶段提交,要么先写Redo Log再写Binlog ,要么先写Binlog再写Redo Log 。这两种顺序在数据库崩溃时都会导致数据库状态与Binlog恢复出来的状态不一致

下面我们仍以更新语句为例:
UPDATE T SET c = c + 1 WHERE ID = 2,当前 c = 0

场景1:先写Redo Log后写Binlog,写完Redo Log后崩溃

执行过程

  1. 写入Redo Log(此时 c = 1 已记录)。
  2. 准备写Binlog,但MySQL进程异常重启(Binlog未写入)。

崩溃恢复后

  • Redo Log已持久化,InnoDB根据Redo Log恢复数据,c = 1
  • Binlog中没有这条更新语句的记录。

后果

  • 主库数据为 c = 1
  • 如果后续使用Binlog恢复从库或进行时间点恢复,由于Binlog缺失这条语句,恢复出来的数据 c = 0
  • 主从不一致 / 备份与当前库不一致
场景2:先写Binlog后写Redo Log,写完Binlog后崩溃

执行过程

  1. 写入Binlog(记录了"将 c 从0改为1")。
  2. 准备写Redo Log,但MySQL进程异常重启(Redo Log未写入)。

崩溃恢复后

  • Redo Log中没有该事务的任何记录,因此数据未被修改,c = 0
  • Binlog中已经存在这条更新语句。

后果

  • 主库数据为 c = 0
  • 如果使用Binlog恢复从库或进行时间点恢复,会多执行一次 更新,恢复出来的数据 c = 1
  • 主从不一致 / 备份与当前库不一致
两阶段提交如何解决这个问题?

两阶段提交将Redo Log的写入拆分为 preparecommit 两个阶段,并与Binlog的写入顺序严格绑定:
Binlog文件 InnoDB引擎 执行器 Binlog文件 InnoDB引擎 执行器 1. 写入Redo Log (prepare) OK 2. 写入Binlog OK 3. 提交事务 (Redo Log commit) 完成

这样,无论在哪个阶段发生崩溃,都能通过崩溃恢复规则保证数据一致性。

崩溃恢复规则

数据库重启后,InnoDB会扫描Redo Log,对每个事务的状态进行判断:

Redo Log状态 Binlog状态 恢复行为
commit 标记 任意 提交事务(Redo Log已完整)
仅有 prepare,无 commit Binlog完整且存在 提交事务(说明Binlog已写入成功)
仅有 prepare,无 commit Binlog不完整或不存在 回滚事务

如何判断Binlog是否完整?

Binlog中每条事务都有明确的开始和结束标记(如 XID_EVENTQUERY_EVENTCOMMIT)。如果读取到完整的事务块,则认为完整。

不同崩溃场景的具体处理

场景A:Redo Log prepare 写入后、Binlog写入前崩溃

  • Redo Log状态:prepare,无 commit
  • Binlog:尚未写入或写入不完整
  • 恢复处理:回滚事务(因为Binlog不完整,从库/备份不应看到这次修改)

场景B:Binlog写入后、Redo Log commit 前崩溃

  • Redo Log状态:prepare,无 commit
  • Binlog:完整存在
  • 恢复处理:提交事务(因为Binlog已经记录了,必须保证主库与从库一致)

场景C:Redo Log commit 后崩溃

  • Redo Log状态:commit
  • Binlog:必然完整(因为commit前Binlog已写入)
  • 恢复处理:直接提交(无需额外动作)
Redo Log与Binlog如何关联?

它们通过一个共同的字段------XID(事务ID)进行关联。

  • 每个事务在开始时会被分配一个唯一的XID。
  • Redo Log的prepare记录中会写入XID。
  • Binlog的记录中也会包含同一个XID。
  • 崩溃恢复时,MySQL顺序扫描Redo Log:
    • 如果遇到commit,直接提交。
    • 如果遇到prepare,则拿着XID去Binlog中查找对应的事务:
      • 找到且完整 → 提交
      • 未找到或不完整 → 回滚
总结一下两阶段提交的核心价值
崩溃时机 Redo Log Binlog 恢复后数据状态 与Binlog是否一致
prepare后、Binlog前 prepare 无/不完整 回滚,数据不变 一致
Binlog后、commit前 prepare 完整 提交,数据已改 一致
commit后 commit 完整 提交,数据已改 一致

通过两阶段提交,MySQL保证了即使在崩溃恢复的场景下,主库的数据状态与Binlog重放出来的状态始终一致,这是主从复制和数据恢复正确性的基石。

5.5 三大日志总结对比

日志类型 所属组件 记录内容 写入方式 核心作用 是否支持事务
Redo Log InnoDB引擎 物理日志:数据页修改 循环写,固定大小 持久性、crash-safe
Undo Log InnoDB引擎 逻辑日志:旧值记录 写入Undo表空间 事务回滚、MVCC
Binlog Server层 逻辑日志:SQL/行变更 追加写,可切换文件 主从复制、时间点恢复 是(所有引擎)

注意:MyISAM等引擎不支持Redo/Undo Log,因此不具备崩溃恢复能力和事务支持。Binlog对所有引擎都可用。

七、小结

一条SQL语句从客户端发出到最终返回结果,历经连接器→查询缓存(过时)→解析器→预处理器→优化器→执行器→存储引擎 。对于查询,存储引擎直接从Buffer Pool或磁盘读取数据返回;对于更新,InnoDB还会精心协调Buffer Pool、Undo Log、Redo Log(两阶段提交)、Binlog以及后台刷脏页,从而在保证高性能的同时,实现事务的ACID特性。

相关推荐
顾凌陵5 小时前
SQL注入漏洞
数据库·sql·oracle
DN金猿5 小时前
SpringCloudAlibaba微服务启动报错
微服务·云原生·nacos·架构·springcloud·sca
无心水5 小时前
【分布式利器:SOAF】蚂蚁开源的金融级微服务全家桶:SOFAStack 核心架构与实战选型对比
人工智能·分布式·微服务·金融·架构·开源·分布式利器
深蓝电商API5 小时前
分布式电商爬虫架构:Scrapy-Redis+消息队列的集群部署
分布式·爬虫·架构
洛水水5 小时前
数据库连接池详解
数据库·c++·mysql
WL_Aurora5 小时前
MySQL慢查询分析与优化实战
mysql·性能优化·慢查询·查询优化
小江的记录本5 小时前
【Java基础】Java 8-21新特性 :JDK17:密封类、模式匹配、Record类(附《思维导图》+《面试高频考点清单》)
java·数据结构·后端·python·mysql·面试·职场和发展
小江的记录本5 小时前
【Java基础】集合框架: ArrayList vs LinkedList 核心区别、扩容机制(附《思维导图》+《面试高频考点清单》)
java·数据库·python·mysql·spring·面试·maven
小挪号底迪滴5 小时前
研发出海实战:全球化系统的时区陷阱、I18n架构与跨国协作避坑指南
架构