MySQL · 一条 SQL 是如何执行的?

SQL 查询执行过程

大体来说,MySQL 可以分为 Server 层和存储引擎层两部分:

  • Server 层:包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能。
    • 连接器:跟客户端建立连接、获取权限、维持和管理连接,建议使用长连接。
    • 查询缓存:(key,value) 方式存储,命中返回、未命中执行下一步,更新时缓存失效,不建议打开。
    • 解析器:进行词法分析、语义分析生成解析树,预处理器会检查解析树的合法性。
    • 优化器:根据代价模型,选择索引和执行顺序,生成执行计划。
    • 执行器:验证权限,调存储引擎接口返回结果,并更新查询缓存。
  • 存储引擎层:与数据库文件打交道,负责数据的存储和读取。

SQL 更新执行过程

与查询流程不一样的是,更新流程还涉及两个重要的日志模块:redo log(重做日志)和 binlog(归档日志)。

背景:MySQL 有个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了 Write-Ahead Logging 思想(它的关键点就是先写日志,再写磁盘)来提升更新效率。

redo log

1
2
-rw-r----- 1 mysql mysql 1.0G Sep 17 07:20 ib_logfile0
-rw-r----- 1 mysql mysql 1.0G Sep 17 07:15 ib_logfile1

说明:记录数据页做了什么改动,而不是这个页更新之后的状态。

作用:有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。

原理:InnoDB 的 redo log 是固定大小的,每个文件的大小是 1GB,从头开始写,写到末尾就又回到开头循环写。write pos 是当前记录的位置,checkpoint 是当前要擦除的位置,如果 write pos 追上 checkpoint,表示满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

参数innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

binlog

1
2
3
4
-rw-r----- 1 mysql mysql 1.1G Sep 11 07:17 mysql-bin.000098
-rw-r----- 1 mysql mysql 1.1G Sep 15 07:17 mysql-bin.000099
-rw-r----- 1 mysql mysql 515M Sep 17 07:20 mysql-bin.000100
-rw-r----- 1 mysql mysql 87 Sep 15 07:17 mysql-bin.index

上面介绍的 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

说明binlog 有两种模式,statement 格式的话是记 sql 语句, row 格式会记录行的内容,记两条更新前和更新后都有。

作用binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式,用于备份恢复。

参数sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数也建议设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

区别

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

update 执行流程

  • 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  • 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  • 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交 commit 状态,更新完成。

redo log 的写入拆成了两个步骤:preparecommit,这就是 “两阶段提交”。简单说,redo logbinlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个数据状态保持逻辑上的一致。