MYSQL服务端的整体架构
可以看到服务端主要由 Server 层和存储引擎两部分组成:
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层负责数据的存储和提取,其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。我们后续教程也主要基于 InnoDB 进行讲解。
查询缓存
当 MySQL 服务端拿到一条 SQL 查询语句后,首先会查询缓存,看之前是不是执行过这条语句。如果执行过,会缓存在内存中,这个时候直接返回之前缓存的查询结果给客户端即可;如果在缓存中没有找到对应的记录,就会继续后面的操作,并且在最终执行完成后,将查询结果保存到查询缓存。
可以看到,如果命中查询缓存,MySQL 不需要执行后面的复杂操作就可以直接返回结果,查询效率会很高。但是通常不建议这么做,原因和是否要在业务逻辑中保存模型类查询结果一样:因为这个缓存的 key 是查询语句,只要有一点不同(实际项目中查询字段、查询条件千姿百态)就会导致缓存命中失败,同时,数据表记录本身也是不断更新(插入、更新、删除)的,更新之后,之前的查询缓存就全部失效了,所以从维护成本和实际收益上看,得不偿失。除非这张表创建初始化后不怎么更新,是一个静态表,并且查询语句相对单一。
注:MySQL 8.0 版本开始将不再支持查询缓存功能。
分析器
如果查询缓存没有启用或者没有命中,就开始真正执行 SQL 查询语句了。
MySQL 会通过分析器对 SQL 语句做词法分析,以确定到底要做什么,比如 select 表示查询语句,update 表示更新语句等,表名是什么,查询的字段有哪些,查询的条件是什么。
确定要做的事情之后,分析器还会对 SQL 语句进行语法分析,以确保符合 MySQL 语法。
优化器
如果 SQL 语句词法和语法分析都没有问题,接下来,会经由优化器生成执行计划,这里面主要的工作是数据表包含索引的时候,判定是否使用索引,以及使用哪些索引效率最高(扫描行数最少),我们可以在执行一个 SQL 查询语句之前通过 explain 语句查看它的执行计划:
执行器
通过分析器可以知道客户端发送的 SQL 语句要做什么,通过优化器可以确定要怎么做,最后就是真正去执行了,这一步通过服务端的执行器完成。
在根据执行计划执行 SQL 查询语句时,会先验证权限,有相应的权限才会继续执行,否则会报权限错误。
在具体执行查询操作时,是通过调用存储引擎提供的 API 接口完成的,MySQL 支持不同的存储引擎,虽然这些存储引擎存取数据的底层实现不尽相同,但是对 Server 层提供了统一的接口,执行器调用这些接口可以完成诸如读取下一行记录、插入记录、更新记录之类的日常数据库操作,执行 SQL 查询返回所有满足条件的结果集也是如此。
在存储引擎中进行查询操作时,如果 SQL 语句没有使用索引,则需要一条条遍历数据表的所有记录(全表扫描),然后将满足条件的记录存放到结果集,直到数据表最后一行,最后再把这个结果集返回给客户端。如果数据表非常大,表记录非常多时,这种查询的效率会很低下,这是我们在优化数据库查询效率时所要极力避免的现象。
SQL 更新语句的执行流程与日志写入
执行流程
和 SQL 查询语句一样,MySQL 客户端提交 SQL 更新语句(表示一个更新请求)前,先要通过连接器建立与服务端的连接,然后就可以执行更新操作了(假设在 test 数据库中已经存在一个 post 数据表,如果没有的话,可以手动创建):
我们在介绍查询缓存的时候提到过,当一张数据表有更新操作时,对应的查询缓存数据会清空,所以上述插入语句会清空 post 表的所有缓存(修改、删除语句也是一样)。
接下来,分析器会通过词法和语法解析知道这是一条 SQL 插入语句,优化器为其生成对应的执行计划,最后,执行器负责具体执行,插入数据(具体操作交由存储引擎去做)。
以上插入语句,如果是想下面这样的修改语句:
1
update post set title='test title 2' where id=1;
连接器和查询缓存这里和插入语句都是一样的,在分析器中会解析出这是一条 SQL 修改语句,由于带有 WHERE 查询条件,因此在优化器生成的执行计划会判定是否使用索引(我们可以通过explain 语句预览执行计划,这里可以看到会使用 id 这个主键索引):
最后,执行器负责具体执行,找到这一行记录,然后进行更新。
与查询流程不一样的是,更新流程还涉及两个重要的日志写入,分别是 redo log(重做日志)和 binlog(归档日志)。
日志写入
我们知道,MySQL 数据库数据是会持久化到磁盘的(在文件系统中有对应的数据目录,关于这一块后面会专门介绍),如果每一次的更新操作都要写入磁盘,整个过程的 IO 成本很高(如果包含查询的话,还有额外的查询成本)。
为了解决这个问题,MySQL 的设计者引入了 WAL 技术(Write-Ahead Logging),即先写日志,再写磁盘。
以 InnoDB 引擎为例,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(重做日志)里面,并更新内存,这个时候更新就算完成了,然后,InnoDB 引擎会在系统空闲的时候,将这个操作记录更新到磁盘里面。
之所以叫做重做日志,是因为如果 MySQL 数据库服务端发生异常崩溃,重启时可以根据这个日志记录的步骤完成未持久化到磁盘的数据更新操作,从而保证数据的一致性。
那为什么又有 binlog 呢?
实际上,redo log 是 InnoDB 引擎提供的日志系统,在 InnoDB 引擎出现之前,MySQL 默认的存储引擎是 MyISAM,那个时候为了实现数据备份和恢复,使用的是 binlog,不过 binlog 是一个归档日志,不具备数据库崩溃重启后的数据恢复功能,因此,InnoDB 专门开发了一套 redo log 日志系统。
注:binlog 是属于 MySQL Server 层的日志系统,因此所有的存储引擎都可以共用它。
下面我们来看看在 InnoDB 引擎中,这两个日志是如何写入的:
执行器通过 API 接口将更新数据传递给存储引擎执行更新操作;
存储引擎在拿到更新数据后,先将其更新到内存,同时将这个更新操作记录到 redo log,此时 redo log 处于 prepare 状态,然后告知执行器执行完成了,随时可以提交事务;
执行器生成这个操作的 binlog,并把 binlog 写入磁盘;
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成 commit 状态,更新完成。
在上述步骤中,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是「两阶段提交」。
如果不使用两阶段提交,会导致两份日志恢复的数据不一致:比如先写 redo log,binlog 还没有写入,数据库崩溃重启;或者先写 binlog,redo 还没有写入数据库崩溃重启,都将造成恢复数据的不一致。
而使用两阶段提交后,就可以保证两份日志恢复的数据一致:只有 binlog 写入成功的情况下,才会提交 redo log,否则 redo log 处于 prepare 状态,事务会回滚,这样一来,就保证了数据的一致性。
另一个需要注意的是,redo log 是循环写(后面的记录会覆盖前面的),不能持久保存全量日志,binlog 是增量写(一直追加写入),可以保存全部归档日志,因此,redo log 主要适用于数据库崩溃后重启的数据恢复,而 binlog 可用于全量备份,以及创建「数据库分身」,实现主从同步。