mysql系列-⼀条SQL查询语句是如何执⾏的?

语言: CN / TW / HK

⼀条SQL查询语句是如何执⾏的?

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

Server 层

Server 层包括连接器、查询缓存、分析器、优化器、执⾏器等,涵盖 MySQL 的⼤多数核⼼服务功能,以及所有的内置函数(如⽇期、时间、数学和加密函数等),所有跨存储引擎的功能都在这⼀层实现,⽐如存储过程、触发器、视图等。

存储引擎层

⽽存储引擎层负责数据的存储和提取。其架构模式是插件式的,⽀持 InnoDB(MySQL 5.5.5版本后默认)、MyISAM、 Memory 等多个存储引擎

连接器

连接经过tcp握手,身份校验、权限校验等

连接建立耗时复杂,尽量减少连接采用长连接。

长连接时MySQL 占⽤内存涨得特别快,这是因为MySQL 在执⾏过程中临时使⽤的内存是管理在连接对象⾥⾯的。这些资源会在连接断开的时候才释放。

解决:

1. 定期断开⻓连接   。使⽤⼀段时间,或者程序⾥⾯判断执⾏过⼀个占⽤内存的⼤查询后,断开连接,之后要查询再重连。

2. 如果你⽤的是 MySQL 5.7 或更新版本,可以在每次执⾏⼀个⽐较⼤的操作后,通过执⾏mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证, 但是会将连接恢复到刚刚创建完时的状态

命令show processlist:查看连接列表

查询缓存

缓存中会保存以查询sql为key,结果为value的键值对数据,查询时先进行缓存查询。

不建议使用缓存,因只要对表更新则此表的所有缓存数据都会清空,缓存命中率低,保存缓存影响系统性能,只适合静态数据表做缓存。

mysql提供动态选择查询是否使用缓存机制,参数 query_cache_type 设置成DEMAND,默认普通查询sql不使用查询缓存,通过SQL_CACHE显示指定查询时使用缓存:

select SQL_CACHE * from T where ID=10;

注:mysql8.0已将缓存功能删除

分析器

词法分析、语法分析,校验sql正确性

优化器

如优化字段条件顺序,索引选择

执行器

表 T 中,ID 字段没有索引,那么执⾏器的执⾏流程是这样的:

1. 调⽤ InnoDB 引擎接⼝取这个表的第⼀⾏,判断 ID 值是不是 10,如果不是则跳过,如果是则将这⾏存在结果集中;

2. 调⽤引擎接⼝取“下⼀⾏”,重复相同的判断逻辑,直到取到这个表的最后⼀⾏。

3. 执⾏器将上述遍历过程中所有满⾜条件的⾏组成的记录集作为结果集返回给客户端。

日志系统:一条SQL更新语句是如何执行的?

与查询流程不⼀样的是,更新流程还涉及两个重要的⽇志模块,它们正是我们今天要讨论的主⻆:redo log(重做⽇志)和 binlog(归档⽇志)

redo log

古代饭店老板有一个记账赊账本,一个小黑板临时记账,客人多时在小黑板上临时记上,客人少的时候再核算移到账本上。每次直接记到账本需要查询大量账目太耗时。

数据库同理,每次更新数据都写磁盘效率太低,数据库充当小黑板功能的就是redo log,redo log和小黑板一样有大小限制,⽐如可以配置为⼀组 4 个⽂件,每个⽂件的⼤⼩是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就⼜回到开头循

环写,如下⾯这个图所示。

write pos 是当前记录的位置,⼀边写⼀边后移。

checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据⽂件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以⽤来记录新的操作。如果 writepos 追上 checkpoint,表示“粉板”满了,这时候不能再执⾏新的更新,得停下来先擦掉⼀些记录,把 checkpoint 推进⼀下。

有了 redo log,InnoDB 就可以保证即使数据库发⽣异常重启,之前提交的记录都不会丢失,这个能⼒称为crash-safe。

即只要写到了redo log中,即使未提交到数据库异常重启后会自动将redo log记录执行的数据刷新到数据库

binlog

MySQL ⾃带引擎 MyISAM,无crash-safe能力,binlog只能用来归档,InnoDB为另一公司插件形式引入,需要实现crash-safe能力,故而引入redo log。

这两种⽇志有以下三点不同。

1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使⽤。

2. redo log 是物理⽇志,记录的是“在某个数据⻚上做了什么修改”;binlog 是逻辑⽇志,记录的是这个语句的原始逻辑,⽐如“给 ID=2 这⼀⾏的 c 字段加 1 ”。

3. redo log 是循环写的,空间固定会⽤完;binlog 是可以追加写⼊的。“追加写”是指 binlog ⽂件写到⼀定⼤⼩后会切换到下⼀个,并不会覆盖以前的⽇志。

Redo log是记录这个页 “做了什么改动”。

Binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。row格式日志会比较大

执⾏器和 InnoDB 引擎在执⾏这个简单的 update语句时的内部流程

1. 执⾏器先找引擎取 ID=2 这⼀⾏。ID 是主键,引擎直接⽤树搜索找到这⼀⾏。如果 ID=2 这⼀⾏所在的数据⻚本来就在内存中,就直接返回给执⾏器;否则,需要先从磁盘读⼊内存,然后再返回。

2. 执⾏器拿到引擎给的⾏数据,把这个值加上 1,⽐如原来是 N,现在就是 N+1,得到新的⼀⾏数据,再调⽤引擎接⼝写⼊这⾏新数据。

3. 引擎将这⾏新数据更新到内存中,同时将这个更新操作记录到 redo log ⾥⾯,此时 redo log处于 prepare 状态。然后告知执⾏器执⾏完成了,随时可以提交事务。

4. 执⾏器⽣成这个操作的 binlog,并把 binlog 写⼊磁盘。

5. 执⾏器调⽤引擎的提交事务接⼝,引擎把刚刚写⼊的 redo log 改成提交(commit)状态,更新完成。

图中浅⾊框表示是在 InnoDB 内部执⾏的,深⾊框表示是在执⾏器中执⾏的。

redo log 的写⼊拆成了两个步骤:prepare 和commit,这就是" 两阶段提交 "。

若不分两阶段,先写redo log或者先写binlog,系统崩溃在两者间隙都可能造成数据库恢复数据和内存读取数据不一致。

两阶段日志都写完后才提交保证两个状态逻辑上一致。

两阶段提交必要性举例:

用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,如果不使用两阶段提交会出现什么情况呢?

先写 redo log 后写 binlog

假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。

先写 binlog 后写 redo log

如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

如果是两阶段提交,这时候redolog只是完成了prepare, 而binlog又失败,那么事务本身会回滚,所以这个库里面status的值是0。

正常执行是要commit 才算完,但是崩溃恢复过程的话,可以接受“redolog prepare 并且binlog完整” 的情况,即最后一步commit异常了再次重启数据库会认可数据写到磁盘

innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。

两个值都建议设置为1,可保证在数据库异常重启后数据不丢失。

sql执行详细过程

1.首先客户端通过tcp/ip发送一条sql语句到server层的SQL interface

2.SQL interface接到该请求后,先对该条语句进行解析,验证权限是否匹配

3.验证通过以后,分析器会对该语句分析,是否语法有错误等

4.接下来是优化器器生成相应的执行计划,选择最优的执行计划

5.之后会是执行器根据执行计划执行这条语句。在这一步会去open table,如果该table上有MDL,则等待。

如果没有,则加在该表上加短暂的MDL(S)

(如果opend_table太大,表明open_table_cache太小。需要不停的去打开frm文件)

6.进入到引擎层,首先会去innodb_buffer_pool里的data dictionary(元数据信息)得到表信息

7.通过元数据信息,去lock info里查出是否会有相关的锁信息,并把这条update语句需要的

锁信息写入到lock info里(锁这里还有待补充)

8.然后涉及到的老数据通过快照的方式存储到innodb_buffer_pool里的undo page里,并且记录undo log修改的redo

(如果data page里有就直接载入到undo page里,如果没有,则需要去磁盘里取出相应page的数据,载入到undo page里)

9.在innodb_buffer_pool的data page做update操作。并把操作的物理数据页修改记录到redo log buffer里

由于update这个事务会涉及到多个页面的修改,所以redo log buffer里会记录多条页面的修改信息。

因为group commit的原因,这次事务所产生的redo log buffer可能会跟随其它事务一同flush并且sync到磁盘上

10.同时修改的信息,会按照event的格式,记录到binlog_cache中。(这里注意binlog_cache_size是transaction级别的,不是session级别的参数,一旦commit之后,dump线程会从binlog_cache里把event主动发送给slave的I/O线程)

11.之后把这条sql,需要在二级索引上做的修改,写入到change buffer page,等到下次有其他sql需要读取该二级索引时,再去与二级索引做merge

(随机I/O变为顺序I/O,但是由于现在的磁盘都是SSD,所以对于寻址来说,随机I/O和顺序I/O差距不大)

12.此时update语句已经完成,需要commit或者rollback。这里讨论commit的情况,并且双1

13.commit操作,由于存储引擎层与server层之间采用的是内部XA(保证两个事务的一致性,这里主要保证redo log和binlog的原子性),

所以提交分为prepare阶段与commit阶段

14.prepare阶段,将事务的xid写入,将binlog_cache里的进行flush以及sync操作(大事务的话这步非常耗时)

15.commit阶段,由于之前该事务产生的redo log已经sync到磁盘了。所以这步只是在redo log里标记commit

16.当binlog和redo log都已经落盘以后,如果触发了刷新脏页的操作,先把该脏页复制到doublewrite buffer里,把doublewrite buffer里的刷新到共享表空间,然后才是通过page cleaner线程把脏页写入到磁盘中

注:其实在实现上5是调用了6的过程了的,所以是一回事。MySQL server 层和InnoDB层都保存了表结构,所以有书上描述时会拆开说

细节:

binglog为逻辑日志,redo log为物理日志

逻辑日志可以给别的数据库,别的引擎使用,已经大家都讲得通这个“逻辑”;

物理日志就只有“我”自己能用,别人没有共享我的“物理格式”

redo log为顺序写,速度快很多

数据库更新操作都是基于内存页,更新的时候不会直接更新磁盘,如果内存有存在就直接更新内存,如果内存没有存在就从磁盘读取到内存,在更新内存,并且写redo log

小惊喜 

1、积少成多,下载高佣联盟,领取各大平台隐藏优惠券,每次购物省个十块八块不香吗,通过下方二维码注册的用户可添加微信liershuang123(微信号)领取价值千元海量学习视频。

为表诚意奉献部分资料:

软件电子书:链接:https://pan.baidu.com/s/1_cUtPtZZbtYTF7C_jwtxwQ 提取码:8ayn

架构师二期:链接:https://pan.baidu.com/s/1yMhDFVeGpTO8KTuRRL4ZsA 提取码:ui5v

架构师阶段课程:链接:https://pan.baidu.com/s/16xf1qVhoxQJVT_jL73gc3A 提取码:2k6j

2、本人重金购买付费前后端分离脚手架源码一套,现10元出售,加微信liershuang123获取源码

分享到: