搜索
您的当前位置:首页正文

详解一条SQL的执行过程

来源:二三娱乐
详解⼀条SQL的执⾏过程

以下⽂章来源于码海 ,作者码海详解⼀条 SQL 的执⾏过程

天天和数据库打交道,⼀天能写上⼏⼗条 SQL 语句,但你知道我们的系统是如何和数据库交互的吗?MySQL 如何帮我们存储数据、⼜是如何帮我们管理事务?....是不是感觉真的除了写⼏个 「select * from dual」外基本脑⼦⼀⽚空⽩?这篇⽂章就将带你⾛进 MySQL 的世界,让你彻底了解系统到底是如何和 MySQL 交互的,MySQL 在接受到我们发送的 SQL 语句时⼜分别做了哪些事情。

我们的系统在和 MySQL 数据库进⾏通信的时候,总不可能是平⽩⽆故的就能接收和发送请求,就算是你没有做什么操作,那总该是有其他的“⼈”帮我们做了⼀些事情,基本上使⽤过 MySQL 数据库的程序员多多少少都会知道 MySQL 驱动这个概念的。就是这个 MySQL 驱动在底层帮我们做了对数据库的连接,只有建⽴了连接了,才能够有后⾯的交互。看下图表⽰

这样的话,在系统和 MySQL 进⾏交互之前,MySQL 驱动会帮我们建⽴好连接,然后我们只需要发送 SQL 语句就可以执⾏ CRUD 了。⼀次 SQL 请求就会建⽴⼀个连接,多个请求就会建⽴多个连接,那么问题来了,我们系统肯定不是⼀个⼈在使⽤的,换句话说肯定是存在多个请求同时去争抢连接的情况。我们的 web 系统⼀般都是部署在 tomcat 容器中的,⽽ tomcat 是可以并发处理多个请求的,这就会导致多个请求会去建⽴多个连接,然后使⽤完再都去关闭,这样会有什么问题呢?如下图

java 系统在通过 MySQL 驱动和 MySQL 数据库连接的时候是基于 TCP/IP 协议的,所以如果每个请求都是新建连接和销毁连接,那这样势必会造成不必要的浪费和性能的下降,也就说上⾯的多线程请求的时候频繁的创建和销毁连接显然是不合理的。必然会⼤⼤降低我们系统的性能,但是如果给你提供⼀些固定的⽤来连接的线程,这样是不是不需要反复的创建和销毁连接了呢?相信懂⾏的朋友会会⼼⼀笑,没错,说的就是数据库连接池。

数据库连接池:维护⼀定的连接数,⽅便系统获取连接,使⽤就去池⼦中获取,⽤完放回去就可以了,我们不需要关⼼连接的创建与销毁,也不需要关⼼线程池是怎么去维护这些连接的。

常见的数据库连接池有 Druid、C3P0、DBCP,连接池实现原理在这⾥就不深⼊讨论了,采⽤连接池⼤⼤节省了不断创建与销毁线程的开销,这就是有名的「池化」思想,不管是线程池还是 HTTP 连接池,都能看到它的⾝影。

数据库连接池

到这⾥,我们已经知道的是我们的系统在访问 MySQL 数据库的时候,建⽴的连接并不是每次请求都会去创建的,⽽是从数据库连接池中去获取,这样就解决了因为反复的创建和销毁连接⽽带来的性能损耗问题了。不过这⾥有个⼩问题,业务系统是并发的,⽽ MySQL 接受请求的线程呢,只有⼀个?其实 MySQL 的架构体系中也已经提供了这样的⼀个池⼦,也是数据库连池。双⽅都是通过数据库连接池来管理各个连接的,这样⼀⽅⾯线程之前不需要是争抢连接,更重要的是不需要反复的创建的销毁连接。

⾄此系统和 MySQL 数据库之间的连接问题已经说明清楚了。那么 MySQL 数据库中的这些连接是怎么来处理的,⼜是谁来处理呢?

⽹络连接必须由线程来处理

对计算基础稍微有⼀点了解的的同学都是知道的,⽹络中的连接都是由线程来处理的,所谓⽹络连接说⽩了就是⼀次请求,每次请求都会有相应的线程去处理的。也就是说对于 SQL 语句的请求在 MySQL 中是由⼀个个的线程去处理的。

那这些线程会怎么去处理这些请求?会做哪些事情?

SQL 接⼝

MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接⼝去处理。

查询解析器

假如现在有这样的⼀个 SQL

SELECT stuName,age,sex FROM students WHERE id=1

但是这个 SQL 是写给我们⼈看的,机器哪⾥知道你在说什么?这个时候解析器就上场了。他会将 SQL 接⼝传递过来的 SQL 语句进⾏解析,翻译成MySQL ⾃⼰能认识的语⾔,⾄于怎么解析的就不需要在深究了,⽆⾮是⾃⼰⼀套相关的规则。

现在 SQL 已经被解析成 MySQL 认识的样⼦的,那下⼀步是不是就是执⾏吗?理论上是这样⼦的,但是 MySQL 的强⼤远不⽌于此,他还会帮我们选择最优的查询路径。

什么叫最优查询路径?就是 MySQL 会按照⾃⼰认为的效率最⾼的⽅式去执⾏查询具体是怎么做到的呢?这就要说到 MySQL 的查询优化器了

MySQL 查询优化器

查询优化器内部具体怎么实现的我们不需要是关⼼,我需要知道的是 MySQL 会帮我去使⽤他⾃⼰认为的最好的⽅式去优化这条 SQL 语句,并⽣成⼀条条的执⾏计划,⽐如你创建了多个索引,MySQL 会依据成本最⼩原则来选择使⽤对应的索引,这⾥的成本主要包括两个⽅⾯, IO 成本和 CPU 成本IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当⽤到某个数据时,并不会只读取这个数据,⽽会把这个数据相邻的数据也⼀起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取⼀整页,⼀页的成本就是 1。所以 IO 的成本主要和页的⼤⼩有关

CPU 成本:将数据读⼊内存后,还要检测数据是否满⾜条件和排序等 CPU 操作的成本,显然它与⾏数有关,默认情况下,检测记录的成本是 0.2。MySQL 优化器 会计算 「IO 成本 + CPU」 成本最⼩的那个索引来执⾏

画外⾳:索引成本具体怎么计算,请参考

优化器执⾏选出最优索引等步骤后,会去调⽤存储引擎接⼝,开始去执⾏被 MySQL 解析过和优化过的 SQL 语句

存储引擎

查询优化器会调⽤存储引擎的接⼝,去执⾏ SQL,也就是说真正执⾏ SQL 的动作是在存储引擎中完成的。数据是被存放在内存或者是磁盘中的(存储引擎是⼀个⾮常重要的组件,后⾯会详细介绍)

本篇⽂章⼤家先对存储引擎有⼀个⼤致的认识就可以了。后续专门⽂章来详细介绍的。

执⾏器

执⾏器是⼀个⾮常重要的组件,因为前⾯那些组件的操作最终必须通过执⾏器去调⽤存储引擎接⼝才能被执⾏。执⾏器最终最根据⼀系列的执⾏计划去调⽤存储引擎的接⼝去完成 SQL 的执⾏

我们以⼀个更新的SQL语句来说明,SQL 如下

UPDATE students SET stuName = '⼩强' WHERE id = 1

当我们系统发出这样的查询去交给 MySQL 的时候,MySQL 会按照我们上⾯介绍的⼀系列的流程最终通过执⾏器调⽤存储引擎去执⾏,流程图就是上⾯那个。在执⾏这个 SQL 的时候 SQL 语句对应的数据要么是在内存中,要么是在磁盘中,如果直接在磁盘中操作,那这样的随机IO读写的速度肯定让⼈⽆法接受的,所以每次在执⾏ SQL 的时候都会将其数据加载到内存中,这块内存就是 InnoDB 中⼀个⾮常重要的组件:缓冲池 Buffer Pool

Buffer Pool (缓冲池)是 InnoDB 存储引擎中⾮常重要的内存结构,顾名思义,缓冲池其实就是类似 Redis ⼀样的作⽤,起到⼀个缓存的作⽤,因为我们都知道 MySQL 的数据最终是存储在磁盘中的,如果没有这个 Buffer Pool 那么我们每次的数据库请求都会磁盘中查找,这样必然会存在 IO 操作,这肯定是⽆法接受的。但是有了 Buffer Pool 就是我们第⼀次在查询的时候会将查询的结果存到 Buffer Pool 中,这样后⾯再有请求的时候就会先从缓冲池中去查询,如果没有再去磁盘中查找,然后在放到 Buffer Pool 中,如下图

按照上⾯的那幅图,这条 SQL 语句的执⾏步骤⼤致是这样⼦的

1. innodb 存储引擎会在缓冲池中查找 id=1 的这条数据是否存在2. 发现不存在,那么就会去磁盘中加载,并将其存放在缓冲池中

3. 该条记录会被加上⼀个独占锁(总不能你在修改的时候别⼈也在修改吧,这个机制本篇⽂章不重点介绍,以后会专门写⽂章来详细讲解)

undo ⽇志⽂件

undo ⽇志⽂件:记录数据被修改前的样⼦

undo 顾名思义,就是没有做,没发⽣的意思。undo log 就是没有发⽣事情(原本事情是什么)的⼀些⽇志

我们刚刚已经说了,在准备更新⼀条语句的时候,该条语句已经被加载到 Buffer pool 中了,实际上这⾥还有这样的操作,就是在将该条语句加载到 BufferPool 中的时候同时会往 undo ⽇志⽂件中插⼊⼀条⽇志,也就是将 id=1 的这条记录的原来的值记录下来。这样做的⽬的是什么?

Innodb 存储引擎的最⼤特点就是⽀持事务,如果本次更新失败,也就是事务提交失败,那么该事务中的所有的操作都必须回滚到执⾏前的样⼦,也就是说当事务失败的时候,也不会对原始数据有影响,看图说话

这⾥说句额外话,其实 MySQL 也是⼀个系统,就好⽐我们平时开发的 java 的功能系统⼀样,MySQL 使⽤的是⾃⼰相应的语⾔开发出来的⼀套系统⽽已,它根据⾃⼰需要的功能去设计对应的功能,它即然能做到哪些事情,那么必然是设计者们当初这么定义或者是根据实际的场景变更演化⽽来的。所以⼤家放平⼼态,把 MySQL 当作⼀个系统去了解熟悉他。

到这⼀步,我们的执⾏的 SQL 语句已经被加载到 Buffer Pool 中了,然后开始更新这条语句,更新的操作实际是在Buffer Pool中执⾏的,那问题来了,按照我们平时开发的⼀套理论缓冲池中的数据和数据库中的数据不⼀致时候,我们就认为缓存中的数据是脏数据,那此时 Buffer Pool 中的数据岂不是成了脏数据?没错,⽬前这条数据就是脏数据,Buffer Pool 中的记录是⼩强 数据库中的记录是旺财 ,这种情况 MySQL是怎么处理的呢,继续往下看

redo ⽇志⽂件:记录数据被修改后的样⼦

除了从磁盘中加载⽂件和将操作前的记录保存到 undo ⽇志⽂件中,其他的操作是在内存中完成的,内存中的数据的特点就是:断电丢失。如果此时MySQL 所在的服务器宕机了,那么 Buffer Pool 中的数据会全部丢失的。这个时候 redo ⽇志⽂件就需要来⼤显神通了

画外⾳:redo ⽇志⽂件是 InnoDB 特有的,他是存储引擎级别的,不是 MySQL 级别的redo 记录的是数据修改之后的值,不管事务是否提交都会记录下来,例如,此时将要做的是update students set stuName='⼩强' where id=1; 那么这条操作就会被记录到 redo log buffer 中,啥?怎么⼜出来⼀个 redo log buffer ,很简单,MySQL 为了提⾼效率,所以将这些操作都先放在内存中去完成,然后会在某个时机将其持久化到磁盘中。

截⾄⽬前,我们应该都熟悉了 MySQL 的执⾏器调⽤存储引擎是怎么将⼀条 SQL 加载到缓冲池和记录哪些⽇志的,流程如下:

1. 准备更新⼀条 SQL 语句

2. MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中

3. 在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo ⽇志⽂件中4. innodb 会在 Buffer Pool 中执⾏更新操作5. 更新后的数据会记录在 redo log buffer 中

上⾯说的步骤都是在正常情况下的操作,但是程序的设计和优化并不仅是为了这些正常情况⽽去做的,也是为了那些临界区和极端情况下出现的问题去优化设计的

这个时候如果服务器宕机了,那么缓存中的数据还是丢失了。真烦,竟然数据总是丢失,那能不能不要放在内存中,直接保存到磁盘呢?很显然不⾏,因为在上⾯也已经介绍了,在内存中的操作⽬的是为了提⾼效率。

此时,如果 MySQL 真的宕机了,那么没关系的,因为 MySQL 会认为本次事务是失败的,所以数据依旧是更新前的样⼦,并不会有任何的影响。好了,语句也更新好了那么需要将更新的值提交啊,也就是需要提交本次的事务了,因为只要事务成功提交了,才会将最后的变更保存到数据库,在提交事务前仍然会具有相关的其他操作

将 redo Log Buffer 中的数据持久化到磁盘中,就是将 redo log buffer 中的数据写⼊到 redo log 磁盘⽂件中,⼀般情况下,redo log Buffer 数据写⼊磁盘的策略是⽴即刷⼊磁盘(具体策略情况在下⾯⼩总结出会详细介绍),上图

如果 redo log Buffer 刷⼊磁盘后,数据库服务器宕机了,那我们更新的数据怎么办?此时数据是在内存中,数据岂不是丢失了?不,这次数据就不会丢失了,因为 redo log buffer 中的数据已经被写⼊到磁盘了,已经被持久化了,就算数据库宕机了,在下次重启的时候 MySQL 也会将 redo ⽇志⽂件内容恢复到 Buffer Pool 中(这边我的理解是和 Redis 的持久化机制是差不多的,在 Redis 启动的时候会检查 rdb 或者是 aof 或者是两者都检查,根据持久化的⽂件来将数据恢复到内存中)

到此为⽌,从执⾏器开始调⽤存储引擎接⼝做了哪些事情呢?1.准备更新⼀条 SQL 语句

2.MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中 3.在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo ⽇志⽂件中4.innodb 会在 Buffer Pool 中执⾏更新操作5.更新后的数据会记录在 redo log buffer 中

---到此是前⾯已经总结过的---6.MySQL 提交事务的时候,会将 redo log buffer 中的数据写⼊到 redo ⽇志⽂件中 刷磁盘可以通过 innodb_flush_log_at_trx_commit 参数来设置值为 0 表⽰不刷⼊磁盘值为 1 表⽰⽴即刷⼊磁盘值为 2 表⽰先刷到 os cache

7.myslq 重启的时候会将 redo ⽇志恢复到缓冲池中

截⽌到⽬前位置,MySQL 的执⾏器调⽤存储引擎的接⼝去执⾏【执⾏计划】提供的 SQL 的时候 InnoDB 做了哪些事情也就基本差不多了,但是这还没完。下⾯还需要介绍下 MySQL 级别的⽇志⽂件 bin log

bin log ⽇志⽂件:记录整个操作过程

上⾯介绍到的redo log是 InnoDB 存储引擎特有的⽇志⽂件,⽽bin log属于是 MySQL 级别的⽇志。redo log记录的东西是偏向于物理性质的,如:“对什么数据,做了什么修改”。bin log是偏向于逻辑性质的,类似于:“对 students 表中的 id 为 1 的记录做了更新操作” 两者的主要特点总结如下:性质

redo Log

bin Log

bin log 可通过配置参数max_bin log_size设置每个bin log⽂件的⼤⼩(但是⼀般不建议修改)。

⽂件redo log 的⼤⼩是固定的(配置中也可以⼤⼩设置,⼀般默认的就⾜够了)

实现redo log是InnoDB引擎层实现的(也就是说是bin log是 MySQL 层实现的,所有引擎都可以使⽅式Innodb 存储引起过独有的)⽤ bin log⽇志记录redo log 采⽤循环写的⽅式记录,当写到⽅式结尾时,会回到开头循环写⽇志。

bin log 通过追加的⽅式记录,当⽂件⼤⼩⼤于给定值后,后续的⽇志会记录到新的⽂件上

使⽤redo log适⽤于崩溃恢复(crash-safe)(这⼀

bin log 适⽤于主从复制和数据恢复

场景点其实⾮常类似与 Redis 的持久化特征)bin log⽂件是如何刷⼊磁盘的?

bin log 的刷盘是有相关的策略的,策略可以通过sync_bin log来修改,默认为 0,表⽰先写⼊ os cache,也就是说在提交事务的时候,数据不会直接到磁盘中,这样如果宕机bin log数据仍然会丢失。所以建议将sync_bin log设置为 1 表⽰直接将数据写⼊到磁盘⽂件中。刷⼊ bin log 有以下⼏种模式

1、 STATMENT

基于 SQL 语句的复制(statement-based replication, SBR),每⼀条会修改数据的 SQL 语句会记录到 bin log 中【优点】:不需要记录每⼀⾏的变化,减少了 bin log ⽇志量,节约了 IO , 从⽽提⾼了性能【缺点】:在某些情况下会导致主从数据不⼀致,⽐如执⾏sysdate()、slepp()等2、ROW

基于⾏的复制(row-based replication, RBR),不记录每条SQL语句的上下⽂信息,仅需记录哪条数据被修改了【优点】:不会出现某些特定情况下的存储过程、或 function、或 trigger 的调⽤和触发⽆法被正确复制的问题【缺点】:会产⽣⼤量的⽇志,尤其是 alter table 的时候会让⽇志暴涨3、MIXED

基于 STATMENT 和 ROW 两种模式的混合复制( mixed-based replication, MBR ),⼀般的复制使⽤ STATEMENT 模式保存 bin log ,对于 STATEMENT模式⽆法复制的操作使⽤ ROW 模式保存 bin log

那既然bin log也是⽇志⽂件,那它是在什么记录数据的呢?

其实 MySQL 在提交事务的时候,不仅仅会将 redo log buffer 中的数据写⼊到redo log ⽂件中,同时也会将本次修改的数据记录到 bin log⽂件中,同时会将本次修改的bin log⽂件名和修改的内容在bin log中的位置记录到redo log中,最后还会在redo log最后写⼊ commit 标记,这样就表⽰本次事务被成功的提交了。

如果在数据被写⼊到bin log⽂件的时候,刚写完,数据库宕机了,数据会丢失吗?

⾸先可以确定的是,只要redo log最后没有 commit 标记,说明本次的事务⼀定是失败的。但是数据是没有丢失了,因为已经被记录到redo log的磁盘⽂件中了。在 MySQL 重启的时候,就会将 redo log 中的数据恢复(加载)到Buffer Pool中。

好了,到⽬前为⽌,⼀个更新操作我们基本介绍得差不多,但是你有没有感觉少了哪件事情还没有做?是不是你也发现这个时候被更新记录仅仅是在内存中执⾏的,哪怕是宕机⼜恢复了也仅仅是将更新后的记录加载到Buffer Pool中,这个时候 MySQL 数据库中的这条记录依旧是旧值,也就是说内存中的数据在我们看来依旧是脏数据,那这个时候怎么办呢?

其实 MySQL 会有⼀个后台线程,它会在某个时机将我们Buffer Pool中的脏数据刷到 MySQL 数据库中,这样就将内存和数据库的数据保持统⼀了。

本⽂总结

到此,关于Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系就基本差不多了。我们再回顾下

1. Buffer Pool 是 MySQL 的⼀个⾮常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的2. Undo log 记录的是数据操作前的样⼦

3. redo log 记录的是数据被操作后的样⼦(redo log 是 Innodb 存储引擎特有)4. bin log 记录的是整个操作记录(这个对于主从复制具有⾮常重要的意义)从准备更新⼀条数据到事务的提交的流程描述

1. ⾸先执⾏器根据 MySQL 的执⾏计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中2. 在数据被缓存到缓存池的同时,会写⼊ undo log ⽇志⽂件

3. 更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中4. 完成以后就可以提交事务,在提交的同时会做以下三件事5. (第⼀件事)将redo log buffer中的数据刷⼊到 redo log ⽂件中6. (第⼆件事)将本次操作记录写⼊到 bin log⽂件中

7. (第三件事)将 bin log ⽂件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记⾄此表⽰整个更新事务已经完成

结束语

到此为⽌,系统是如何和 MySQL 数据库打交道,提交⼀条更新的 SQL 语句到 MySQL,MySQL 执⾏了哪些流程,做了哪些事情从宏观上都已经讲解完成了。更多的 Buffer Pool 的细节将会在之后的⽂章中详解

因篇幅问题不能全部显示,请点此查看更多更全内容

Top