这次我们从MySQL的整体架构来讲SQL的执行过程,如下图:
在整体分为两部分Server和引擎层,这里引擎层我使用InnoDB去代替,引擎层的设计是插件形式的,可以任意替代,接下来我们开始介绍每个组件的作用:
连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接;
查询缓存:服务的查询缓存,如果能找到对应的查询,则不必进行查询解析,优化,执行等过程,直接返回缓存中的结果集;
解析器:解析器会根据查询语句,构造出一个解析树,主要用于根据语法规则来验证语句是否正确,比如SQL的关键字是否正确,关键字的顺序是否正确;
优化器:解析树转化为查询计划,一般情况下,一条查询可以有很多种执行方式,最终返回相同的结果,优化器就是根据成本找到这其中最优的执行计划;
执行器:执行计划调用查询执行引擎,而查询引擎通过一系列API接口查询到数据;
后台线程:负责刷新内存池中的数据,保证缓存池中的内存缓存是最近的数据,将已修改的数据刷新到磁盘文件,同时保证数据库发生异常的情况能恢复到正常情况;
内存池:内存池也可以叫做缓存池,主要为弥补磁盘的速度较慢对数据库产生的影响,查询的时候,首先将磁盘读到的页的数据放在内存池中,下次读取的时候直接从内存池中读取数据,修改数据的时候,首先修改内存池中的数据,然后后台线程按照一定的频率刷新到磁盘上。
文件:主要是指表空间文件,而外还有一些日志文件;
以上大致的介绍一下MySQL的整体架构,其中内存池、文件、后台线程等一些跟细节的东西没有介绍,后面我们介绍其他时候在带出来其中的详细的部分,另外在附上一张MySQL5.6整体架构图:
这部分内容是建立在上部分的基础上,需要对内存池、文件、后台线程深入到细节去了解组成,接下我们还是分三部分开始讲解:
文件分为日志文件和存储文件,分为两部分讲起:
存储文件也就是表数据的存储,整体的存储结构如下图:
表空间主要分为两类文件,一类是共享表空间,一类是每张表单独的表空间,单独的表空间存放的是表中的数据、索引等信息,共享的表空间主要是存储事务信息、回滚信息等数据;表空间由段(Segment)、区(Extend)、页(Page)、行(Row)组成,接下来简单介绍一下这4种结构:
关于日志文件这里主要介绍三种日志文件,分别为binlog、redo log、redo log:
binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。binlog是mysql的逻辑日志,并且由Server层进行记录,使用任何存储引擎的mysql数据库都会记录binlog日志。binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。
binlog的主要使用场景有两个,分别是主从复制和数据恢复;
对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,Mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N,
N代表多少条以后开始进行刷盘,当设置为0的时候由系统自行判断何时写入磁盘,当设置为1的时候,相当于每次Commit就进行刷盘一次,但是这个时候要注意与redo log日志可能存在不一致的情况,这个时候需要设置innodb_support_xa参数也为1,这样就能保证两个两份日志是同步的。
redo log包括两部分:redo log buffer和redo log file,redo log buffer是在内存中,redo log file是在磁盘上,当MySQL执行DML语句的时候,首先写入redo log buffer,然后按照一定条件顺序写入redo log file,什么时候会触发buffer内容写入到file当中呢?
redo log记录数据页的变更,在设计上redo log采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志,本质上就是一个环状。
rdo log刷盘完成以后,其实数据最终还没刷新到真正数据磁盘上,因此还需要刷新到真正的数据磁盘上,本质上redo log的设计就是为了降低对数据页刷盘的要求,接下来我们结合上图来聊聊是如何刷新到数据文件文件上的,也就是checkpoint机制:
首先看下环,环上有4个ib_logfile_*的文件,该文件就是存储redo log日志的文件,可以通过控制innodb_log_files_in_group的数量来控制文件的个数,通过innodb_log_file_size来控制文件的大小,不介意将文件的设置的太大,如果设置的太大会导致奔溃恢复的时候过于缓慢,也不能设置的太小,这样可能导致一次事务需要切换多次日志文件,此外还会照成频繁写入磁盘文件,照成性能抖动;
接下来我们看两个端点write pos和check point,write pos到check point之间的部分是redo log空着的部分,用于记录新的记录;check point到write pos之间是redo log待落盘的数据页更改记录。当write pos追上check point时,会先推动check point向前移动,空出位置再记录新的日志。
InnoDB在启动的时候,不管上次数据库是否正常关闭,都会尝试进行恢复操作,分为两种情况:
在恢复的过程中因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如binlog)要快很多;
MySQL用来确保事务的持久性。redo log记录事务执行后的状态,用来恢复未写入data file的已成功事务更新的数据。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
undo log记录数据的逻辑变化,用户事务的回滚操作和MVCC, undo log 存放在共享表空间中,以段(rollback segment)的形式存在。
逻辑格式的日志,在事务进行回滚的时候,可以将数据从逻辑上恢复至事务之前的状态。
保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由Purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。
InnoDB 存储引擎是基于磁盘存储的,也就是说数据都是存储在磁盘上的,由于 CPU 速度和磁盘速度之间的鸿沟, InnoDB 引擎使用缓冲池技术来提高数据库的整体性能。内存池简单来说就是一块内存区域.在数据库中进行读取页的操作,首先将从磁盘读到的页存放在内存池中,下一次读取相同的页时,首先判断该页是不是在内存池中,若在,称该页在内存池中被命中,直接读取该页。否则,读取磁盘上的页。对于数据库中页的修改操作,首先修改在内存池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘。
内存池中缓存的信息主要有:index page、data page、insert buffer、自适应哈希索引、 lock info、数据字典信息等。索引页和数据页占缓冲池的很大一部分。在InnoDB中,内存池中的页大小默认为16KB,和磁盘的页的大小默认一样。我们已经介绍过数据文件的存储结构相信大家对缓存结构的内容也会有一定理解,我们就不单独介绍了,后面只会重点强调一下insert buffer和自适应哈希索引这两块内容,以及扩展下内存池的设计原理。
Insert Buffer的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,不存在,则先放入一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多,这就大大提高了对于非聚集索引插入的性能。这个时候可能会照成一种情况,当MySQL数据库发生宕机的时候有有大量的Insert Buffer没有被合并到非聚集索引的页当中的时候,这个时候MySQL恢复需要很长的时间。
需要满足的条件:
索引是非聚集索引,索引不是唯一的;
对于具体的实现我们下次再聊;
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以提升速度,这简历哈希索引,称之为自适应哈希索引。AHI是通过缓冲池的B+树页构造而来的。因此建立的速度非常快,且不要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动的为某些热点页建立哈希索引。
这是最核心的一个线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括赃页的刷新、合并插入缓冲等。
在 InnoDB 存储引擎中大量使用了异步 IO 来处理写 IO 请求, IO Thread 的工作主要是负责这些 IO 请求的回调处理。
事务被提交之后, undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用并分配的 undo页. InnoDB 支持多个 Purge Thread, 这样做可以加快 undo 页的回收。
完成整体功能介绍以后,我们开始聊聊数据如何插入到InnoDB引擎上的:
假设场景如下:
首先我们创建一张表T,主键为Id,辅助索引为a
create table T(id int primary key, a int not null, name varchar(16),index (a))engine=InnoDB;
接下来插入一条数据,
insert into t(id,a,name) values(id1,a1,’哈哈’),(id2,a2,’哈哈哈’);
我们介绍过MySQL读取数据的流程,Server层我们还是会经过连接器、解析器、优化器、执行器这些东西,这些我们就不介绍了,我们主要介绍剩下的操作:
插入数据时候可能有两种场景:
第一种场景:假设Id1这条数据在内存池中,
第二种场景假设id2这条数据不再内存池中,
我们来聊聊内存池(Buffer Pool)运行原理,可以从以下3个方面来看: