说到 SQL 语句是如何执行的,那么是离不开 MySQL 的 架构的,本文涉及到的存储引擎都指的是 InnoDB,MySQL5.5 以后默认的存储引擎
MySQL 的架构
接下来我根据上图描述一下各个组件的作用
连接器
连接器的作用主要就是进行身份验证和权限相关,当客户端发起连接的时候,MySQL 服务会将用户的所有权限信息封装到连接中,用于此连接所有相关权限的认证,比如我们常说的只读/读写权限就是和这个相关。
查询缓存
这个就是一个结果集的缓存,key
是 SQL 语句,value
是查询结果,但是在 MySQL8 的版本中就删除了这个功能。那么为什么删除了这个功能?
虽然它能提高查询效率,但是它的缺点也非常的明显。
key
必须完全匹配,而且对大小写敏感,不匹配不会使用缓存。now()
,它也不会缓存,因为结果不确定。所以我们就能得出一些它非常显著的缺点以至于它被移除掉了,频繁失效,内存开销大,数据不一致。
分析器
一个 SQL 进来我得知道是干嘛的对吧,分析器就是干这个的,主要分为两部分:
第一部分:此法解析: 提取关键词,比如 SELECT
,UPDATE
,提出查询的表,查询条件等等,就是将你的 SQL 语句分割成一个块一块的。
第二部分:语法解析: 判定你的 SQL 语句是不是符合 MySQL 的语法。
优化器
当你的SQL满足了 MySQL 的语法规则,同时我也将 SQL 的各个部分都分割出来了,那么下一步就是要进行优化,虽然你很牛逼,但是没有它牛逼(它认为),它会解析你的 SQL 然后拿出它认为的最优的执行方案去执行,比如如何选择索引,多表查询如何选择关联顺序,但是也不一定是最优的,毕竟它不懂得人情世故。
执行器
优化器得到执行计划之后,执行器就可以干活了,再执行SQL 语句的时候它会先根据你连接里面的权限进行判断,来决定能否执行,如果有权限,就调用引擎的接口去执行了。
上面就是整个执行的大致过程。对于简单的查询来说没什么好说的,查询大多数还是和索引相关,所以我会单抽出来一篇文章来说MySQL 中非常重要的索引部分。但是一条更新语句的执行过程还是要说的,但是涉及到 MySQL 里面日志的部分,所以我先说一下 MySQL 中的三大日志。
MySQL 的三大日志
MySQL中的日志还是比较多的,比如查询日志,慢查询日志,错误日志,事务相关的日志,和二进制日志几个部分,而最主要的就是三种日志,分别是binlog(归档),redolog(重做)和 undolog(回滚)。
redolog 重做日志
MySQL中数据是以页的为单位,在 Innodb 存储引擎中,一页的大小是 16kb,当去查询一条记录的时候,会从硬盘把一页的数据拿到 Buffer Pool 中,然后 MySQL 的任何读写其实都是操作的 Buffer Pool,这个很好理解,内存比磁盘那可是快多了。所以这里一定是有 InnoDB 存储引擎和磁盘之间的交互的,直接上个图。
更新完 BufferPool 中的数据之后,会将更新记录写入到 redolog buffer 中,每一条 redolog 记录都是由表空间号,数据页号,便宜量,修改数据长度,具体修改的数据组成的,在 MySQL 服务中是不会直接操作磁盘文件的,因为很慢,所以都会在内存中开辟一个空间作为缓存,然后通过缓存不断的刷入到磁盘中,这个过程也叫做刷盘,刷盘也要经过文件缓存系统,这个和操作系统有关了,然后InnoDB会调用 fsync,将数据刷新到redofile中,那么刷盘的时机是什么呢?
redolog 的刷盘时机
它的刷盘时机可不是固定的一个逻辑,而是区分了很多种情况。
事务提交: 当事务提交的时候会讲redolog buffer 中的数据刷到文件中,这个需要一个参数的控制,innodb_flush_log_at_trx_commit
,这个参数有三种:
当设置为 0
的时候就是事务提交不刷盘,而是周期性的每隔一秒进行刷盘,所以如果 MySQL 宕机了,这 1s
的数据可能会丢失掉。
当设置为 1
的时候,也就是默认的时候表示每次提交事务都进行刷盘,只要事务提交成功,那么就会在磁盘中。
当设置为 2
的时候表示每次事务提交的时候只把buffer
中的数据写入到文件缓存系统中,但是不调用 fsync
,还是交给后台线程去完成这个事。
log buffer 空间不足: 当 log Buffer
中的数据到了总容量一半左右的时候就会进行刷盘了。
事务日志缓冲区满了: InnoDB 使用一个事务日志缓冲区来暂时存储事务的重做日志条目,当缓冲区满的时候会触发日志的刷新,然后写入磁盘
检查点: InnoDB 会定期执行检查点,就是将内存中已经修改但是尚未写入磁盘的数据页刷新到磁盘,并且将 redolog 一起刷新,保持数据的一致性
后台刷新: InnoDB后台启动一个线程,每隔 1
秒会将已经修改但是没写入磁盘的数据刷新到磁盘,同时一起刷新重做日志
正常关闭服务: 正常关闭的时候,redolog 都会写入到磁盘中
所以从上面我们发现,InnoDB 做了非常多的事情,就是为了保证数据的持久化和一致性。同时也可以发现,因为一些其他的操作,可能会导致我们事务还没有提交的数据也会刷入到磁盘中,比如我们的长事务。
在磁盘中的 redolog 文件是以一个文件组的形式存在的,而且是一个环形数组,大家可以想象成一条蛇在转圈,追着自己的尾巴,这里有两个变量,一个是 write pos
,当前记录的位置,一个是 checkpoint
要擦除的位置。redolog
最后还是要更新实际的磁盘数据的,每次有新的 redolog
写入的时候 writepos
都会后移(蛇头开始动),每次写完磁盘实际数据checkpoint
后移(蛇尾开始动),所以两个变量中间的区域就是可以写入的数据区域,一旦追上了,就说明,没有地方写 redolog 了,那么就需要 MySQL 停下来,让数据写一写,空闲一下区域才行。
扩展: MySQL 8.0.30
版本以前可以设置文件组中文件的个数和每个文件的大小来决定文件组的空间大小,但是这个版本之后不是了,而是固定死,文件的个数是 32
,然后去设置总容量innodb_redo_log_capacity
,所以每个文件的大小就变成了innodb_redo_log_capacity/32
。
为什么要用 redolog 刷盘?而不是直接将 Buffer Pool 修改的数据直接刷呢?
在 BufferPool
中存储的是以页为单位的数据,也就是 16kb
,在这里面如果不通过 redolog是不知道修改了什么数据的,如果直接将整页的数据刷盘,性能太低了,当然了 BufferPool
也会在一定时机刷盘,后面说。
binlog
binlog
不同于redolog,是 MySQL 服务层的日志,而且是逻辑的,也就是语句的原始逻辑,不管你的引擎是什么,只要发生了表数据的修改,都会产生 binlog。它的作用就是存储所有实际到数据变化的逻辑操作,而且是顺序写。在数据备份,主从,主备等等都是依靠它来完成的。
binlog 有三种格式,这三种格式就标志着在 binlog 中的存储内容。
now()
,在 statement
模式下就直接写成 a=now()
这种,但是在 row
中会使用 MySQL binlog 工具,转换成时机更新的值,同时还会记录这个更新的行数据的所有字段的原始值,这种方式很可靠,但是非常的费空间,毕竟要存所有字段的原始值。所以就产生了第三种模式。 row
,否则就用 statement
。刷盘时机
它的刷盘时机就比较简单了,万变不离其宗,还是会有一个 binlog 的 cache
,但是这个是每个事务私有的,每个事务都有自己的缓存区,当事务提交的时候然后刷盘到文件缓存系统,然后调用 fsync
刷到文件中,调用 fsync
可以通过 sync_binlog
控制,设置为 0
表示只写入文件缓存系统,然后让系统自行判断啥时候刷到磁盘。设置为 1
表示事务一提交那么两个动作一起做了,如果你的 IO
有瓶颈,可以设置大一点,这个数就是表示多少个事务之后执行fsync
刷到磁盘。如果宕机,这几个事务的就没了。
小总结一下: 别把 binlog当成恢复数据的日志文件,它不是干这个的,干这个的是 redolog,binlog 存的东西磁盘根本就读不懂,redolog 记录着数据更新的表空间,数据页的信息,它是崩溃恢复的关键,binlog 是让数据库集群数据一致的关键,别搞错了。
两阶段提交
介绍完这两个日志之后,我们再回到之前说更新一条数据的事,现在我们知道了,更新一条数据,两个日志都会记录的,都以事务为单位,但是 redolog 是在事务执行的时候就可以不断的写入了,但是 binlog 是事务提交的时候才写入,两个时机不同,那么就会引发问题,当然问题一定是发生在系统异常的情况下的。
redolog 写入了,binlog 写入的时候系统异常了,那么恢复的时候 redolog 的数据就恢复了,但是 binlog 就少了一条这个数据的更新。所以就引申出了两阶段提交。
这个两阶段提交很牛逼,在分布式事务中也会体现,其实就是相互之间的一种保证。理论差不多都一样,就是分成两个阶段,准备和提交。也就是写入 redolog 的时候会写入阶段准备,然后事务提交,写入 binlog,在写入 redolog 提交阶段,即便你 binlog 写入时异常,那么 redolog 恢复时发现是准备阶段那么就回滚了,同理,redolog 提交阶段 写入异常,但是发现你 binlog 写入成功了,那么我就不回滚了。
undolog
说了半天回滚,咋回滚,你 redolog 不一定记录数据原始值,binlog 又是原始逻辑语句,所以 undolog 的作用就出来了,新增一条数据的时候记录主键,回滚的时候直接删除就行了。删除的时候记录记录删除的内容,回滚的时候插入就行了,修改一条记录,记录修改之前的值,回滚时直接更新。
BufferPool中的刷盘策略
这个还是有必要说一下的,上面也埋了坑。上面我说过 BufferPool
中会存在已经修改但是没有提交的数据页,这个也叫脏页。Innodb 使用了一种适应性的刷新算法,根据 redolog 的生成速度和当前刷新率动态调整刷新速度,当刷新数据到磁盘的时候会根据数据页找到 redologfile 中的记录然后抹除掉,省得二次写入磁盘。