一条SQL语句执行步骤
来源于MySQL实战
mysql> select * from T where ID=10;
查询语句执行分析
MySQL分为Server层和存储引擎层
-
连接器:
负责跟客户端建立链接、获取权限、维持和管理连接- 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
- 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
查询缓存:
MySQL拿到一个查询请求后,会先到查询缓存内看看,之前是否执行过这条语句。之前的语句及其结果以key-value对的形式缓存在内存中。
大多数情况下不建议使用查询缓存,因为查询缓存失效非常频繁。只要表中有数据更新,则这个表上的所有查询缓存都会被清空。
静态表适合查询缓存。分析器:
如果没有命中查询缓存,则开始真正执行语句。
分析器先进行词法分析,主要识别sql语句中各个字符串代表的含义。然后进行语法分析,根据语法规则判断输入语句是否满足MySql语法。优化器:
在表里有多个索引时,决定用哪个索引。或者多表关联(join)时,决定各个表的连接顺序。执行器:
MySql通过分析器知道了要做什么,通过优化器知道了该怎么做,下一步就进入执行器开始执行语句。
1.先判断对表T有没有执行权限;
2.如果有权限,则根据表中引擎定义,使用引擎提供的相关接口。
更新语句执行分析
- 首先可以确认的是查询语句的那一套流程,更新语句也是同样走一遍。与查询流程不一样的是,更新流程还涉及两个重要的日志模块。也就是下面要分析的两个主角:redo log(重做日志)、binlog(归档日志)
- 为什么会有两份日志呢?因为最开始并没有InnoDB引擎,数据库自带的引擎是MyISAM,这个引擎并没有crash-safe能力,所有后续的InnoDB使用了另外一套日志
- 两种日志存在三点不同。
1.redo log属于引擎日志,binlog属于服务层日志;
2.redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如"给ID=2这一行的c字段加1";
3.redo 是循环写,binlog可以追加写;
- redo log(引擎层日志)
- 如果每次更新操作,数据库都需要查询磁盘,然后找到对应的记录,再更新记录,整个过程IO成本、查找成本很高。基于以上情况,MySql先顺序落盘,然后等空闲时在更新数据。MySql的WAL(Write-Ahead Logging)技术,关键点就是先写日志(循序写盘),再写磁盘。
- 数据库中InnoDB的redo log是固定大小的,在循环使用时,需要保证被擦掉的数据已经更新到磁盘中。有了redo log,InnoDB就可以保证数据库发生异常重启,之前提交的记录都不会丢失,这就是crash-safe
- binlog(Server层日志)
mysql> update T set c=c+1 where ID=2;
- 执行器先找引擎取ID=2这一行。引擎判断这一行所在数据页是否在内存中,是则直接返回,否则先从磁盘读入内存,然后再返回;
- 执行器拿到行数据后,更新行数据,并调用引擎接口写入数据;
- 引擎更新内存中数据页,并将更新操作记录到redo log里面,此时redo log处于prepare状态。告之执行器执行完成了,可以随时提交事务。
- 执行器生成该操作的binlog,并把binlog写入磁盘;
5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
以下是相关执行流程图:
⚠️大家可能注意到,最后三步看上去有点“绕”,将redo log的写入拆分成两个步骤:prepare和commit,这就是“两阶段提交”
怎样让数据库恢复到半月内任意一秒的状态?
仍然用前面的 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,与原库的值不同。
MySQL 使用两阶段提交主要解决 binlog 和 redo log 的数据一致性的问题。
崩溃恢复规则
redo log 和 binlog 有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:
如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
binlog无记录,回滚事务
binlog有记录,提交事务。