MySQL中如何执行一句SQL
MySQL大体上的结构分为两部分,服务层和引擎层
服务层包括了连接器、缓存、分析器、优化器、执行器; 这一层涵盖了大部分的服务功能
引擎层是可插拔的存储引擎,支持InnoDB、MyISAM等多种存储引擎
MySQL结构解析
连接器
连接器负责和客户端建立连接、获取权限、维持、管理连接等。
在完成TCP握手后,连接器开始验证身份,如果用户名密码认证通过,会获取当前拥有权限。这断开本次连接之前,权限都不会改变。
这就意味着,只要连接建立,那这次连接的权限就无法修改。
对于一个MySQL连接,它会把执行中的临时内存管理在连接对象中,这些资源会在连接断开时才释放,所以一个长连接累计下来会导致内存占用过大的问题。
针对这一问题,可以采用两种方案:
- 定期断开连接,释放资源。
- 在执行一次较大的操作后,执行
mysql_reset_connection
来初始化连接;这个过程不需要重建连接和重新鉴权,会把连接重置到刚刚创建完的状态。
缓存
MySQL在拿到一个查询语句的时候,会先到缓存中查看是否之前执行过这条查询语句。之前执行过的语句以及结果,被保存在缓存中,就直接把结果返回客户端。
如果语句不在缓存,就会继续后面的流程,最后会把结果存入缓存。
MySQL的缓存看上去很美好,但它其实很不实用!
查询缓存的过程是极容易失效的,因为只要当前查询的表被更新了,那么这个表上的缓存结果就会被清空。所以除非是一张静态表,或者更新频率很低,那这才会适合使用查询缓存。
幸运的是,MySQL提供了选择,可以使用参数query_cache_type=DEMAND
。这样默认的SQL语句就不会使用缓存,而对于需要缓存的查询, 可以用select SQL_CACHE * from xxx
这样的方式来指定。
需要注意的是,MySQL8.0删除了缓存模块,整个功能都没有了
大多数应用会把缓存做在应用级,比如MyBatis、redis、memcache等;这样直接在内存中查询缓存,少了一次网络I/O,效率更高了。也就不会在MySQL中使用缓存了。
分析器
分析器是MySQL把一句SQL解析的过程。
第一步,词法分析,把一句SQL分成多个词,赋予每一个词意义;即识别每一个词,比如把“select”识别为查询,把“table_A”识别为表A,把“id”识别为字段等。
第二部,语法分析,确保一句SQL符合MySQL的语法规范。
优化器
经过分析器分析后,MySQL已经知道这句SQL要干嘛了。但在开始执行之前, 还要经过优化器。
它的作用是在一个表中有多个索引的情况下决定使用哪个索引;以及一个语句有多表连接时,决定表的连接顺序。
执行器
开始执行时,会先判断当前连接对该操作的表有无对应的读写权限,没有就返回没有权限的错误。(在查询缓存中,会在结果返回前做鉴权)
如果通过,就会打开表,然后根据表的引擎定义,去调用相应的引擎提供的接口。
日志模块redo log
在MySQL中,如果每一次更新操作都直接写入磁盘,然后在磁盘中找到响应位置更新记录,整个过程的IO成本,查找成本都很高。
MySQL采用WAL技术,全称是Write-Ahead Logging, 它的关键就是先记录操作日志, 再写入磁盘。
redo log记录的是“在某个数据页做了什么修改”
当一个记录需要进行更新操作时, InnoDB引擎会先把记录写到redo log中,并更新内存,这样这个操作就算完成了。 然后InnoDB会在适当的时候,将操作记录更新到磁盘里, 这个更新往往就是在系统比较空闲的时候。
在InnoDB中,redo log是固定大小的,整个redo log由一块块小的文件组成。结构类似与一个循环队列或者循环链表,从头开始写,写到末尾再回到开头循环写。采用类似于双指针追逐算法来控制是否要将记录更新到硬盘。redo log被提交后,写入磁盘(redo log持久化默认策略)。由于redo log是顺序写入磁盘的,所以开销不会很大。
日志模块binlog
上面的redo log日志是在InnoDB中的,服务层也有它的日志,称为binlog(归档日志)
这是因为以前的MyISAM没有日志记录能力,只能依靠binlog实现crash-safe(安全宕机)
binlog是可追加写入的,不像redo log只有固定大小
一条update语句在InnoDB引擎中的流程:
- 执行器通过引擎找到id=1的这一行,id为主键,引擎直接用树搜索到这一行,如果id=2这条数据所在的数据页本来就在内存中,就直接返回给执行器;否则把这个数据页读入内存,再返回。
- 执行器拿到引擎给的行数据,把这个值+1,那么旧值就是N,新值为N+1,将新的数据通过引擎写入这行。
- 引擎将这个数据写入(更新内存中数据页),同事将操作记录写到redo log,此时redo log的状态是prepare。然后告知执行器执行完,可以进行事务提交
- 执行器生成这个操作的binlog,并把binlog写入磁盘
- 执行器调用引擎的事务提交接口,引擎把刚刚写入的redo log状态改为commit,更新完成。
redo log的prepare和commit两种状态
为什么要给redo log设置两种状态呢?采用两阶段提交,这是为了让两份日志之间的逻辑一致。
一个事务必须要将两个日志都写完,才算完成提交。这样就保证了两份日志的同步。
两阶段提交,是跨系统维持数据逻辑一致性常用的方法。