从这一章开始,将通过一个系列完整的介绍研发人员需要知道的MySQL知识。
先通过整体流程图从全局上了解一条SQL语句在MySQL中的执行过程,建立整体概念,帮助你从高维度理解问题。

大致了解相关知识点即可,通过后续的文章将会逐步讲解各个环节。

范围

本系列文章知识默认基于MySQL 5.7版本InnoDB引擎,若涉及8.0版本将特殊说明。

更新语句的整体流程图

mysql 审批流程 库表设计 mysql流程图_mysql

下面依次介绍下每个步骤的作用

Server 层

1. 连接器
  1. 校验用户名密码
  2. 从权限表获取用户拥有的权限并设置
    之后这个连接里面的权限判断逻辑,都依赖此时读到的权限,即时用户权限在此期间发生变化,也只有再新建连接的时候才会生效
  3. 建立连接(半双工通信【#注1】,边查询边发送), 管理连接
  4. mysql 审批流程 库表设计 mysql流程图_mysql 审批流程 库表设计_02

  5. 连接成功后,如果客户端没有后续动作,这个连接就处于空闲状态,show processlist可以看到Comman列为Sleep。
    客户端如果长时间不发送Command到Server端,Server端会主动断开连接,超时时间由参数wait_timeout和interactive_timeout控制,默认为8小时。
  6. mysql 审批流程 库表设计 mysql流程图_数据库_03


  7. mysql 审批流程 库表设计 mysql流程图_数据_04

  8. 关于长连接与短连接
    长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
    由于建立连接过程比较复杂、耗时,建议使用长链接,减少建立连接的次数。
    但由于MySQL在执行过程中临时使用的内存是管理在连接对象中,这些资源会在连接断开时才释放。如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是MySQL异常重启了,解决该问题的方案如下:
  1. 定期断开长连接或者程序里面判断执行过一个占用内存的大查询后,断开连接,后续查询再重连。
  2. MySQL 5.7及以上版本,可以通过执行mysql_reset_connection来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

2. 查询缓存

当为select语句时,之前执行过的语句及结果以key-value对的形式被直接缓存到内存中,以SQL为key,查询结果为value。

  1. 如果命中,先做权限验证,然后返回结果。
  2. 如果未命中则执行后面的步骤,执行完成后将结果写入查询缓存。

大多数情况下,查询缓存都较为鸡肋
因为只要对表执行一次更新,不管更新了多少数据,这个表上所有的查询缓存都会被清空。所以对于更新压力大的数据库来说,查询缓存的命中率会非常低。费劲存起来的缓存可能还没怎么使用,就被一个更新全清空了。

# 查询缓存相关的status变量
mysql>SHOW GLOBAL STATUS LIKE 'QCache\_%';
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Qcache_free_blocks      | 1        |  --查询缓存中可用内存块的数目。
| Qcache_free_memory      | 33268592 |  --查询缓存的可用内存量。
| Qcache_hits             | 121      |  --从QC中获取结果集的次数。
| Qcache_inserts          | 91       |  --将查询结果集添加到QC的次数,意味着查询已经不在QC中。
| Qcache_lowmem_prunes    | 0        |  --由于内存不足而从查询缓存中删除的查询数。
| Qcache_not_cached       | 0        |  --未缓存的查询数目。
| Qcache_queries_in_cache | 106      |  --在查询缓存中注册的查询数。
| Qcache_total_blocks     | 256      |  --查询缓存中的块总数。

# 查询缓存命中率 ≈ Qcache_hits / (Qcache_hits + Qcache_hits + Qcache_not_cached) * 100%

查询缓存QC的大小只有几MB,不适合将缓存设置得太大,由于在更新过程中需要线程锁定QueryCache,因此对于非常大的缓存,可能会看到锁争用问题。

但存在即合理,可以将my.cnf参数query_cache_type设置为2(DEMAND)用户自定义模式,在特殊场景下查询时加上 SQL_CACHE 关键字来使用查询缓存,其他的则默认不使用,以下场景适用查询缓存:

  1. 相同的查询是由相同或多个客户机重复发出的。
  2. 被访问的底层数据本质上是静态或半静态的(极少更新)。
  3. 查询有可能是资源密集型和/或构建简短但计算复杂的结果集,同时结果集比较小。
  4. 并发性和查询QPS都不高。

实际业务中大多数时候很难满足上述条件,通常是配置表,数据字典表等静态表才适合,不过此情况也可以考虑通过配置管理系统或者缓存中间件来实现。

# 例如对城市表使用SQL_CACHE关键字来命中查询缓存
select SQL_CACHE * from citys

查询缓存在MySQL 5.6(默认禁用), 5.7(废弃), 8.0(移除)

最后注意:在线上判断SQL执行效率时,最好加上 SQL_NO_CACHE 显示指定不使用查询缓存,才能拿到真实的执行时间。

select SQL_NO_CACHE id,username from userinfo where username='Alice'

3. 分析器

到这步就说明要开始真正的执行语句了,因此需要对SQL语句做解析,包括词法分析,语法分析,步骤如下:

  1. 词法分析: 由MySQLSQLLex完成
  2. 语法分析: 由Bison生成(参考https://en.wikipedia.org/wiki/LR_parser)
  3. 语义分析
  4. 构造执行树

这一步会判断表是否存在,列是否存在等问题

4. 优化器

优化器主要是计算各种执行方式的成本,确定最终的执行方案,可能包含以下方案一种会多种:

  1. 重写查询,优化查询条件顺序
  2. 决定表的读取顺序
  3. 计算使用各索引以及全表扫描的成本进行比较
  4. 计算各种表连接顺序的成本
  5. 生成选择索引后的最终执行计划

优化器不关心表使用什么存储引擎,但是存储引擎对于优化器是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销以及表的统计信息。可以通过关键字提示(hint)或 force index来影响优化器的决策过程。

5. 执行器

  1. 校验表的查询权限
    思考:为什么不在优化器或分析器阶段校验权限呢?因为SQL语句要操作的表不只是SQL字面上那些。比如如果有个触发器,得在执行器阶段(过程中)才能确定。之前的阶段是无能为力的。
  2. 调用存储引擎接口完成查询

在慢查询日中有一个rows_examined字段,表示这个语句执行过程中扫描了多少行。这个值就是执行器每次调用引擎获取数据行的时候累计的。

有些场景下,执行器调用一次,在引擎内部扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的。

引擎层

6. 加载数据页到Buffer Pool

  1. 这里会先判断Buffer Pool里是否有该数据, 若没有则从磁盘读取
  2. MySQL是页为单位从磁盘读取数据, 一次I/O可以读取多页

7. 写入Undo Log(回滚日志)

  1. 记录更新前的旧值。
  2. 如果事务提交失败要回滚数据,可以用Undo Log里的数据恢复Buffer Pool里的缓存数据

8. 修改Buffer Pool

  1. 将SQL变更的结果更新到Buffer Pool。
  2. 根据不同的事务隔离级别结合数据的版本号,客户端可能可以读到不同版本的数据,具体逻辑需要理解 MVCC(多版本并发控制)。
  3. 此时磁盘上的数据还没有发生变化,依然是更新前的值

9. 写Redo Log(重做日志)

  1. 记录Redo Log
  2. Redo Log 打上 Prepare 状态

关于记录Redo Log:

  1. 这里 Redo Log是先写入Redo Log Buffer。
  2. 然后以一定的策略将Buffer中的数据刷到磁盘
    InnoDB 的 Redo Log 是固定大小的,比如可以配置为一组 4 个文件,每个文件 的大小是 1GB,从头开始写,写到末尾就 又回到开头循环写。
  3. mysql 审批流程 库表设计 mysql流程图_mysql 审批流程 库表设计_05

  4. write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件 开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录 更新到数据文件。
    write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如 果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下 来先擦掉一些记录,把 checkpoint 推进一下。
    如果服务宕机,使用Redo Log恢复Buffer Pool,避免丢失数据(关于具体在各种情况下如何恢复数据,由于是面向后端研发人员,此处没有展开说明)

控制Redo Log刷盘策略的参数叫 innodb_flush_log_at_trx_commit:

  1. innodb_flush_log_at_trx_commit = 0
    InnoDB 中的Log Thread 没隔1 秒钟会将log buffer中的数据写入到文件,同时还会通知文件系统进行文件同步的flush操作,保证数据确实已经写入到磁盘上面的物理文件。但是,每次事务的结束(commit 或者是rollback)并不会触发Log Thread 将log buffer 中的数据写入文件。
    所以,当设置为0 的时候,当MySQL Crash 和OS Crash或者主机断电之后,最极端的情况是丢失1 秒时间的数据变更。
  2. innodb_flush_log_at_trx_commit = 1
    这也是InnoDB的默认设置。我们每次事务的结束都会触发Log Thread将log buffer中的数据写入文件并通知文件系统同步文件。
    这个设置是最安全的设置,能够保证不论是MySQL Crash 还是OS Crash或者是主机断电都不会丢失任何已经提交的数据。
  3. innodb_flush_log_at_trx_commit = 2
    当我们设置为2 的时候,Log Thread 会在我们每次事务结束的时候将数据写入事务日志,但是这里的写入仅仅是调用了文件系统的文件写入操作。而我们的文件系统都是有缓存机制的,所以Log Thread的这个写入并不能保证内容真的已经写入到物理磁盘上面完成持久化的动作。文件系统什么时候会将缓存中的这个数据同步到物理磁盘文件Log Thread 就完全不知道了。
    所以,当设置为2 的时候,MySQL Crash 并不会造成数据的丢失,但是OS Crash或者是主机断电后可能丢失的数据量就完全控制在文件系统上了。各种文件系统对于自己缓存的刷新机制各不一样,大家可以自行参阅相关的手册。
    建议设置为1,这样MySQL不会丢失数据,损失一点性能,保证数据安全。

10. 写入Bin Log日志

  1. 执行器写入Bin Log 并执行 fsync(刷盘)
  2. Bin Log的写入由Server完成,所有引擎均有,上述的Undo Log, Redo Log为InnoDB特有

Bin Log是MySQL Server层实现的二进制日志,有以下三种格式:

statement(记录会修改数据的原始SQL语句)

  • 优点:
  1. binlog文件较小
  2. 日志是包含用户执行的原始SQL,方便统计和审计
  • 缺点:
  1. 存在安全隐患,可能导致主从不一致
  2. 对一些系统函数不能准确复制或是不能复制

row(记录被修改的记录值)

记录行的内容,记两条,更新 前和更新后都有。

  • 优点:
  1. 相比statement更加安全的复制格式
  2. 在某些情况下复制速度更快(SQL复杂,表有主键)
  3. 系统的特殊函数也可以复制
  4. 更少的锁
  5. 在复制时,对于更新和删除语句检查是否有主键,如果有则直接执行,如果没有,看是否有二级索引,如再没有,则全表扫描
  • 缺点:
  1. Bin Log比较大(MySQL5.6支持binlog_row_image)
  2. 单语句更新(删除)表的行数过多,会形成大量Bin Log
  3. 无法从Bin Log看见用户执行SQL

mixed(是以上两种level的混合使用)

是以上两种level的混合使用,一般的语句修改使用statment格式保存Bin Log,如一些函数,statement无法完成主从复制的操作,则采用row格式保存Bin Log,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。新版本的MySQL中队row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录。至于update或者delete等修改数据的语句,还是会记录所有行的变更。

Redo Log 是循环写的,空间固定会用完,BinLog 是可以追加写入的。“追加写”是指 Bin Log 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

Bin Log同样有控制刷盘时机的参数 sync_binlog,可配置值说明如下:

  1. sync_binlog = 0
    表示当事务提交之后,MySQL不做fsync之类的磁盘同步指令刷新binlog_cache中的信息到磁盘,而让Filesystem自行决定什么时候来做同步,或者cache满了之后才同步到磁盘。
  2. sync_binlog = 1
    表示每次事务的 Bin Log 都持久化到磁盘
  3. sync_binlog = n
    表示当每进行n次事务提交之后,MySQL将进行一次fsync之类的磁盘同步指令来将binlog_cache中的数据强制写入磁盘。
    建议设置为1,保证 MySQL 异常重启之后数据不丢失,损失一点性能,保证数据安全。

注意:Undo Log是逻辑日志,可以理解为:

  • 当delete一条记录时,Undo Log是中会记录一条对应的insert记录
  • 当insert一条记录时,Undo Log是中会记录一条对应的delete记录
  • 当update一条记录时,它记录一条对应相反的update记录

11. 写入 commit 状态到Redo Log

  1. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 Redo Log 改成提交(commit)状态,更新完成。
  2. 此时已返回客户端告知事务提交成功,实际上磁盘数据还未更新,即存在脏页。

恢复步骤

  1. 对于活跃的事务,直接回滚
  2. 对于Redo Log中是Prepare状态的事务,如果Bin Log中已记录完成则提交,否则回滚事务

12. 随机写入磁盘, 以Page为单位

  1. InndoDB内部基于一定的策略不定期刷盘,将Buffer Pool的数据持久化到磁盘。
  2. 磁盘的随机读/写和顺序读/写在性能上有巨大差异, MySQL数据本身是逻辑有序而不是物理有序。
  3. Redo Log、Undo Log、Bin Log是顺序写入,性能较高。

注解

注1:半双工通信

指允许数据在两个方向上传输,但是同一时间数据只能在一个方向上传输。
另【全双工】指:允许数据在两个方向上同时传输。

系列文章

下一篇:【MySQL优化(二)】性能监控分析 - Show Profile