字节三面:详解一条 SQL 的执行过程

前言

每天和数据库打交道,一天能写上几十条 SQL 语句,但你知道咱们的系统是如何和数据库交互的吗?MySQL 如何帮咱们存储数据、又是如何帮咱们管理事务?....是否是感受真的除了写几个 「select * from dual」外基本脑子一片空白?
这篇文章就将带你走进 MySQL 的世界,让你完全了解系统究竟是如何和 MySQL 交互的,MySQL 在接受到咱们发送的 SQL 语句时又分别作了哪些事情。java

MySQL 驱动

咱们的系统在和 MySQL 数据库进行通讯的时候,总不多是无缘无故的就能接收和发送请求,就算是你没有作什么操做,那总该是有其余的“人”帮咱们作了一些事情,基本上使用过 MySQL 数据库的程序员多多少少都会知道 MySQL 驱动这个概念的。就是这个 MySQL 驱动在底层帮咱们作了对数据库的链接,只有创建了链接了,才可以有后面的交互。看下图表示程序员

字节三面:详解一条 SQL 的执行过程

这样的话,在系统和 MySQL 进行交互以前,MySQL 驱动会帮咱们创建好链接,而后咱们只须要发送 SQL 语句就能够执行 CRUD 了。一次 SQL 请求就会创建一个链接,多个请求就会创建多个链接,那么问题来了,咱们系统确定不是一我的在使用的,换句话说确定是存在多个请求同时去争抢链接的状况。咱们的 web 系统通常都是部署在 tomcat 容器中的,而 tomcat 是能够并发处理多个请求的,这就会致使多个请求会去创建多个链接,而后使用完再都去关闭,这样会有什么问题呢?以下图web

字节三面:详解一条 SQL 的执行过程

java 系统在经过 MySQL 驱动和 MySQL 数据库链接的时候是基于 TCP/IP 协议的,因此若是每一个请求都是新建链接和销毁链接,那这样势必会形成没必要要的浪费和性能的降低,也就说上面的多线程请求的时候频繁的建立和销毁链接显然是不合理的。必然会大大下降咱们系统的性能,可是若是给你提供一些固定的用来链接的线程,这样是否是不须要反复的建立和销毁链接了呢?相信懂行的朋友会会心一笑,没错,说的就是数据库链接池。数据库

数据库链接池:维护必定的链接数,方便系统获取链接,使用就去池子中获取,用完放回去就能够了,咱们不须要关心链接的建立与销毁,也不须要关心线程池是怎么去维护这些链接的。缓存

字节三面:详解一条 SQL 的执行过程

常见的数据库链接池有 Druid、C3P0、DBCP,链接池实现原理在这里就不深刻讨论了,采用链接池大大节省了不断建立与销毁线程的开销,这就是有名的「池化」思想,无论是线程池仍是 HTTP 链接池,都能看到它的身影。tomcat

数据库链接池

到这里,咱们已经知道的是咱们的系统在访问 MySQL 数据库的时候,创建的链接并非每次请求都会去建立的,而是从数据库链接池中去获取,这样就解决了由于反复的建立和销毁链接而带来的性能损耗问题了。不过这里有个小问题,业务系统是并发的,而 MySQL 接受请求的线程呢,只有一个?服务器

其实 MySQL 的架构体系中也已经提供了这样的一个池子,也是数据库连池。双方都是经过数据库链接池来管理各个链接的,这样一方面线程以前不须要是争抢链接,更重要的是不须要反复的建立的销毁链接。网络

字节三面:详解一条 SQL 的执行过程

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

网络链接必须由线程来处理

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

字节三面:详解一条 SQL 的执行过程

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

SQL 接口

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

查询解析器

假如如今有这样的一个 SQL

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

可是这个 SQL 是写给咱们人看的,机器哪里知道你在说什么?这个时候解析器就上场了。他会将 SQL 接口传递过来的 SQL 语句进行解析,翻译成 MySQL 本身能认识的语言,至于怎么解析的就不须要再深究了,无非是本身一套相关的规则。

字节三面:详解一条 SQL 的执行过程

如今 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」 成本最小的那个索引来执行

字节三面:详解一条 SQL 的执行过程

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

存储引擎

查询优化器会调用存储引擎的接口,去执行 SQL,也就是说真正执行 SQL 的动做是在存储引擎中完成的。数据是被存放在内存或者是磁盘中的(存储引擎是一个很是重要的组件,后面会详细介绍)

执行器

执行器是一个很是重要的组件,由于前面那些组件的操做最终必须经过执行器去调用存储引擎接口才能被执行。执行器最终最根据一系列的执行计划去调用存储引擎的接口去完成 SQL 的执行

字节三面:详解一条 SQL 的执行过程

初识存储引擎

咱们以一个更新的SQL语句来讲明,SQL 以下

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

当咱们系统发出这样的查询去交给 MySQL 的时候,MySQL 会按照咱们上面介绍的一系列的流程最终经过执行器调用存储引擎去执行,流程图就是上面那个。在执行这个 SQL 的时候 SQL 语句对应的数据要么是在内存中,要么是在磁盘中,若是直接在磁盘中操做,那这样的随机IO读写的速度确定让人没法接受的,因此每次在执行 SQL 的时候都会将其数据加载到内存中,这块内存就是 InnoDB 中一个很是重要的组件:缓冲池 Buffer Pool

Buffer Pool

Buffer Pool (缓冲池)是 InnoDB 存储引擎中很是重要的内存结构,顾名思义,缓冲池其实就是相似 Redis 同样的做用,起到一个缓存的做用,由于咱们都知道 MySQL 的数据最终是存储在磁盘中的,若是没有这个 Buffer Pool 那么咱们每次的数据库请求都会磁盘中查找,这样必然会存在 IO 操做,这确定是没法接受的。可是有了 Buffer Pool 就是咱们第一次在查询的时候会将查询的结果存到 Buffer Pool 中,这样后面再有请求的时候就会先从缓冲池中去查询,若是没有再去磁盘中查找,而后再放到 Buffer Pool 中,以下图

字节三面:详解一条 SQL 的执行过程

按照上面的那幅图,这条 SQL 语句的执行步骤大体是这样子的

  1. innodb 存储引擎会在缓冲池中查找 id=1 的这条数据是否存在
  2. 发现不存在,那么就会去磁盘中加载,并将其存放在缓冲池中
  3. 该条记录会被加上一个独占锁(总不能你在修改的时候别人也在修改吧,这个机制本篇文章不重点介绍,之后会专门写文章来详细讲解)

undo 日志文件:记录数据被修改前的样子

undo 顾名思义,就是没有作,没发生的意思。undo log 就是没有发生事情(本来事情是什么)的一些日志

咱们刚刚已经说了,在准备更新一条语句的时候,该条语句已经被加载到 Buffer pool 中了,实际上这里还有这样的操做,就是在将该条语句加载到 Buffer Pool 中的时候同时会往 undo 日志文件中插入一条日志,也就是将 id=1 的这条记录的原来的值记录下来。

这样作的目的是什么?

Innodb 存储引擎的最大特色就是支持事务,若是本次更新失败,也就是事务提交失败,那么该事务中的全部的操做都必须回滚到执行前的样子,也就是说当事务失败的时候,也不会对原始数据有影响,看图说话

字节三面:详解一条 SQL 的执行过程

这里说句额外话,其实 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 为了提升效率,因此将这些操做都先放在内存中去完成,而后会在某个时机将其持久化到磁盘中。

字节三面:详解一条 SQL 的执行过程

截至目前,咱们应该都熟悉了 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 数据写入磁盘的策略是当即刷入磁盘(具体策略状况在下面小总结出会详细介绍),上图

字节三面:详解一条 SQL 的执行过程

若是 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 的记录作了更新操做” 二者的主要特色总结以下:

字节三面:详解一条 SQL 的执行过程

bin log文件是如何刷入磁盘的?

bin log 的刷盘是有相关的策略的,策略能够经过sync_bin log来修改,默认为 0,表示先写入 os cache,也就是说在提交事务的时候,数据不会直接到磁盘中,这样若是宕机bin log数据仍然会丢失。因此建议将sync_bin log设置为 1 表示直接将数据写入到磁盘文件中。

刷入 bin log 有如下几种模式

一、 STATMENT

基于 SQL 语句的复制(statement-based replication, SBR),每一条会修改数据的 SQL 语句会记录到 bin log 中

【优势】:不须要记录每一行的变化,减小了 bin log 日志量,节约了 IO , 从而提升了性能

【缺点】:在某些状况下会致使主从数据不一致,好比执行sysdate()、slepp()等

二、ROW

基于行的复制(row-based replication, RBR),不记录每条SQL语句的上下文信息,仅需记录哪条数据被修改了

【优势】:不会出现某些特定状况下的存储过程、或 function、或 trigger 的调用和触发没法被正确复制的问题

【缺点】:会产生大量的日志,尤为是 alter table 的时候会让日志暴涨

三、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 标记,这样就表示本次事务被成功的提交了。

字节三面:详解一条 SQL 的执行过程

若是在数据被写入到bin log文件的时候,刚写完,数据库宕机了,数据会丢失吗?

首先能够肯定的是,只要redo log最后没有 commit 标记,说明本次的事务必定是失败的。可是数据是没有丢失了,由于已经被记录到redo log的磁盘文件中了。在 MySQL 重启的时候,就会将 redo log 中的数据恢复(加载)到Buffer Pool中。

好了,到目前为止,一个更新操做咱们基本介绍得差很少,可是你有没有感受少了哪件事情尚未作?是否是你也发现这个时候被更新记录仅仅是在内存中执行的,哪怕是宕机又恢复了也仅仅是将更新后的记录加载到Buffer Pool中,这个时候 MySQL 数据库中的这条记录依旧是旧值,也就是说内存中的数据在咱们看来依旧是脏数据,那这个时候怎么办呢?

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

字节三面:详解一条 SQL 的执行过程

本文总结

到此,关于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 的细节将会在以后的文章中详解

相关文章
相关标签/搜索