1. 概述
MySQL的系统架构中,可以分为:客户端、服务端、存储引擎三个部分。其示意图如下:
今天我们主要关注一下MySQL Server
的部分,关于存储引擎部分可以看下“MySQL存储引擎介绍”。
下面将从查询语句和更新语句的执行过程来介绍MySQL Server都有哪些组成部分。
2. 查询语句的执行
假设我们有一条普通的查询语句,MySQL内部是如何执行它的呢? 查询语句如下:
select * from user where id = 123;
下面是MySQL的基本架构图,我们从中可以看到SQL语句的执行过程
2.1. 连接器
要使用MySQL,第一步就是连接到数据库,可以使用mysql -u root -p
命令使客户端和mysql服务端建立连接,这个时候会使用到连接器。连接器负责和客户端建立TCP连接、获取权限、维护和管理连接
。
2.2.1. wait_timeout
客户端和MySQL服务端建立连接之后,如果客户端长时间不使用这个连接,MySQL会自动断开这个连接,具体时间是通过配置参数wait_timeout
来控制,默认为8个小时。
如果在连接被断开之后,再基于这个连接发起请求,就会报错。若想再请求需要创建新的连接。
这里请思考一下,为啥我们有些跑批程序(或者执行间隔很长,超过wait_timeout
的应用)貌似没有遇到过这个问题,这是因为使用了数据库连接池了,数据库连接池解决这类问题有两个思路:
- 配置空闲连接的存活时间,存活时间小于数据库的
wait_timeout
即可; - 连接空闲多久(时间一般都可配置)之后就往数据库请求
'select 1 from dual'
,数据库收到请求后会重置wait_timeout
。
2.2.2. 长连接和短连接
在数据库中,如果客户端建立起一个连接之后,持续有请求且都是使用同个连接则称为长连接
。而一个连接只请求几次就断开连接,这种称为短连接
。
因此,一般而言数据库连接池由于实现了连接复用,所以是长连接。而JDBC(只要未实现连接复用)则是短连接。
2.2. 查询缓存
查询缓存是是个K-V对的形式,key是查询的sql,value是查询的结果。在开启查询缓存的情况下,建立好连接后,就会去查询缓存中看要查询的语句是否在缓存当中。如果在缓存中,就直接给客户端返回。
一般来说,不推荐使用查询缓存,主要是因为查询缓存的释放很频繁
。只要对某个进行了更新,所有基于这张表的查询缓存都将会失效。另外在MySQL 8.0中已经将查询缓存模块删除。
2.3. 分析器
分析器模块主要包括两大功能:
- 词法分析
- 语义分析
一般先做词法分析,词法分析完之后再进行语义分析。
词法分析是从是输入的语句select * from user where id = 123
中,将select
关键字识别出来,代表着这是条查询语句,将user
识别成表,将id
识别成列。
语义分析是分析你输入的SQL语句是否符合SQL语法规范。
2.4. 优化器
我们的语句经过分析器之后,MySQL就知道了你想做什么,但是在具体执行之前,还要先经过优化器优化。
优化器主要是生成执行计划,它会根据语句在多个索引中选择最合适的索引
,在有join的语句中,决定各个表的连接顺序。
2.5. 执行器
进行执行器就是要执行语句了,不过在执行之前,会先判断你是否有对这个表的执行权限。
有权限的话,执行器先查询表的存储引擎是什么,查询到存储引擎,就调用存储引擎提供的接口
来执行查询,然后再获取存储引擎返回的结果。
3. 更新语句的执行
假设现在要执行以下更新SQL,我们一起来看看其执行过程
update user set name = 'zs' where id =123;
和查询语句的执行流程一样,其也会先进行连接,连接成功后,分析器分析出是一条更新语句,优化器选择id索引,执行器找到id=123的记录然后再进行更新。
除了上面同样的流程之外,由于更新语句涉及到了数据的更改,所以MySQL未更新语句的执行引入日志模块。主要涉及到两个日志文件: redo和binlog
3.1. redo
在了解MySQL是如何执行一笔更新操作之前,如果我们自己来实现,我们可能会这样做:更新操作直接写进磁盘,然后磁盘先找到对应的记录,再将其更新。这种方式当然可行,不过它的IO成本比较高(随机写)。
MySQL采用的是一种称为WAL(Write-Ahead Logging,日志先行)
的方案,即MySQL会将要更新的记录先写到日志文件(即redo log)中,并更新内存,然后就可以返回客户端更新成功,最后在系统比较空闲的时候再将内存中(buffer pool)的操作记录更新到磁盘(数据实际存储的文件)中。
redo log是Innodb中的日志文件,有了redo文件,即使MySQL服务突然宕机了,可以在系统恢复之后,根据redo log文件的内容进行重做。从而保证了之前的记录不会丢失。这种能力我们称之为“crash safe”
3.1.1. redo log相关参数
我们可以通过show variables like "%innodb_log%";
命令查看和redo log有关的系统参数,主要有以下几个:
innodb_log_file_size ## 指定redo log日志文件的大小
innodb_log_files_in_group ##redo log日志文件组中文件的数量,默认2个,ib_logfile0和ib_logfile1
innodb_log_buffer_size ##redo log buffer的大小
3.1.2. redo log示意图
例如我们配置为一组4个文件,每个文件的大小是1G,那么一共就可以记录4G的记录。redo log是个环形的写入方式,从头开始写,写到末尾又从头开始写,以下是示意图:
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是redo log还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示redo log满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
3.1.3. 写redlog为啥会快
看到这里,不知道你有没有这样的疑惑:写redo log日志为啥笔直接写磁盘要快?
redo log是直接追加写,它是顺序写,而直接写磁盘是随机写
。
连续 I/O 比随机 I/O 效率高的原因是:在做连续 I/O 的时候,磁头几乎不用换道,或者换道的时间很短;而对于随机 I/O,如果这个 I/O 很多的话,会导致磁头不停地换道,造成效率的极大降低。
3.2. binlog
binlog
是记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。它采用追加写
的方式来记录日志。
binlog
的主要作用有:恢复(删库了进行恢复)、复制(主从架构中,从主节点复制到从节点)、审计(判断是否有对数据库进行注入攻击)
有了redo log
,我们可以保证并发事务操作下以及数据库异常时数据页中数据的一致性。那为啥还需要有binlog日志呢?这是因为redo log是属于Innodb存储引擎的,而binlog属于MySQL Server
。先有MySQL,后有Innodb,而binlog日志只能用于归档、数据的实时备份。binlog是不具有crash save
的功能。
3.3. update语句的执行流程
下面是update语句的执行流程图,和查询语句相同的执行流程这里不再体现,直接从执行器开始。
从图上我们可以看到,写redolog
分成了prepare
和commit
两步,这就是 两阶段提交
。
3.4. 两阶段提交
MySQL之所以使用两阶段提交,是因为使用两阶段提交可以保障数据库状态和用日志恢复出来的数据库状态一致
。
假如不是使用两阶段提交,我们分析下会有什么问题:
- 先写
redo log
,再写binlog
,中间发生crash的话,redo log就会有更新记录,而binlog丢失了该次更新。重启之后由于redo log有记录,所以数据能够恢复,而使用binlog恢复的备份库会缺少更新的数据。 - 先写
binlog
,再写redo log
,这种情况正好相反,同步备库时binlog里有更新的记录,所以备库有这条更新的记录,而redo log没有记录,所以主库就丢了本次更新。
两阶段提交也可能会发生异常,它的异常点有:1.写入binlog发生异常,2. 提交事务发生异常。
-
若在写入binlog日志时发生异常:
由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。 -
若binlog日志写完之后发生了异常:
崩溃恢复时的判断规则:
如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交。
如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:- a. 如果是,则提交事务
- b. 否则,回滚事务