MySQL系列学习笔记(一)

一条SQL查询语句的执行过程
写在开始

本系列源自极客时间MySQL专栏,整理而成

在执行下面这个查询语句时的执行的流程是怎么样的?

mysql> select * from table where id=10;

看过相关资料的同学都可能知道执行流程大概是这样的:

其执行过程为:连接、查询缓存、词法分析,语法分析,语义分析,构造执行树,生成执行计划、执行器执行计划。

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

MySQL的逻辑架构图

MySQL的逻辑架构图

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

存储引擎层则负责数据的存储和查询。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

连接器

第一步,你会先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。

在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。

  1. 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
  2. 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。

一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。

客户端连接之后,连接线程在整个查询周期有不同的状态,有下面这几种状态:

  • sleep:线程等待客户端发送查询请求。
  • query:线程正在执行查询过正在向客户端返回查询结果。
  • block:线程正在等待表锁,这个是在 service 层实现的,在存储引擎级别实现的 锁,例如 InoDB 引擎的行锁,不会体现在线程状态中,而对于 MyISAM 来说,这是一个典型的状态。
  • block:线程正在等待表锁,这个是在 service 层实现的,在存储引擎级别实现的 锁,例如 InoDB 引擎的行锁,不会体现在线程状态中,而对于 MyISAM 来说,这是一个典型的状态。
  • analyzing and statistis:线程正在收集存储引擎的统计信息,并生成查询的的执行计划。
  • copying to tmp table:线程正在执行查询,并将结果复制到一个临时表中,这个状 态一般是执行 group by 操作 、文件排序操作、union 操作。
  • sorting Result:线程正在对查询结果进行排序操作。
  • sending data:线程可能处于多个状态间传送数据,比如生成结果集或者在向客户端返回数据。

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。

查询缓存

连接建立完成后,你就可以执行 select语句了。执行逻辑就会来到第二步:查询缓存。

MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。这个过程是根据对大小写敏感的哈希查找实现的,之前执行过的语句及其结果可能会以 键值对的形式直接缓存在内存中。key是查询的语句,value 则是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 只需要进行一次权限检查后就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。

你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。但是大多数情况下建议不要使用查询缓存,为什么呢?

因为查询缓存往往弊大于利。

查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。例如,一个系统配置表,那这张表上的查询才适合使用查询缓存。

分析器

如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。

分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。

MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“table”识别成“表名“table”,把字符串“id”识别成“列 “id”。

因为在sql/lex.h中定义了MySQL关键字和函数关键字,用两个数组存储。
关键字 static SYMBOL symbols[]
函数 static SYMBOL sql_functions[]

做完了这些识别以后,MySQL 会生成一颗”解析树”。
之后通过预处理对生成的”解析树”做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。MySQL的语法分析是使用yacc。
详情参考这篇文章:MySQL 语法解析

如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒。

优化器

经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。

开始执行的时候,要先判断一下你对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。

mysql> select * from table where id = 1 ;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'table'

如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。比如我们这个例子中的表 table 中,id 字段没有索引,那么执行器的执行流程是这样的:

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 id 值是不是 1,如果不是则跳过,如果是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

至此,这个查询语句就执行完成了


一条SQL查询语句的执行过程

query和update执行流程不一样的在于 update涉及了日志模块,binlog (归档日志)和 redo log (重做日志);

学习 MySQL 这两个词肯定是绕不过的,那就先说说这两个日志模块。
日志模块: redo log

在 MySQL 中也是一样,如果每一次更新都需要更新到磁盘,那么去磁盘中查询到这条记录,然后更新数据,整个更新过程的 IO 成本和查找成本都会很高。

为了解决这个问题,MySQL 的设计者就用了类似先记录后更新的思路来提升效率。 就是 MySQL 中常说的 WAL 技术,也就是 write-ahead-logging ,他的关键就是先写日志再写磁盘。

具体点说,更新一条记录的时候,InnoDB 会先写 redo log 并更新到内存中,这时候就算完成了,InnoDB 的引擎会在适当的时候将这条记录更新到磁盘里面,而这个更新往往是系统比较空闲的时候去做。

redo log 是有固定大小的,并且是循环写入的,有了 redo log, InnoDB 就可以保证即使数据库发生异常之后重启,之前提交的记录也不会丢失,这个能力称之为creash-safe.

日志模块 binlog

上面说过 MySQL 整体来看分成 Service 层和存储引擎层,redo log是属于存储引擎层的,而 binlog 属于 Service 层的日志。 可能有些人会问,为什么需要两个日志呢?这是因为一开始MySQL 还没有 InnoDB 存储引擎,只有 MyISAM,这个引擎没有creash-safe 的能力,binlog 只用于归档,而 InnoDB 是通过插件形式引入 MySQL 中的,所以 InnoDB 为了实现 creash-safe 的能力,使用了另外一套日志系统,那就是 redo log 。

那这两种日志有什么不同?

  • redo log是有固定大小的,如果空间满了,就会擦除部分内容才能继续写入,binlog 是追加写的,是指 binlog 文件达到一定大小之后新建文件继续的写入,不会覆盖之前的内容。
  • redo log是 InnoDB 特有的,binlog 是属于 Service层的,所有的存储引擎都可以使用。
  • redo log是属于「物理日志」,记录的是“在某个数据页做了什么修改”; binlog 是属于「逻辑日志」,记录的是原始逻辑,比如“将id = 2的这行数据的 c 字段修改为 3”。

上面说了两种日志的概念和区别,那他们是怎样记录的呢,拿下面的更新语句说起,id 是 table 表的一个主键。

mysql> update table set c = c+1 where id = 2;
  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 内部操作,深色是执行器中的操作;

update 语句执行流程

图片来源极客时间 MySQL 专栏

上面最后三步将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

两阶段提交

为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update
语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

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

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

如何数据恢复

前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。

数据库一天一备和一周一备的区别在哪里?

区别在故障恢复的时间上,一周一备使用binlog重放的语句较长!需要根据业务故障恢复需要的时长定义备份重要性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值