一、MySQL的发展历史和版本分支
MySQL发展历史:
时间 | 里程碑 |
---|---|
1996年 | MySQL1.0发布。它的历史可追溯到1979年,作者Monty用Basic设计的一个报表工具 |
1996年10月 | 3.11.1发布。MySQL没有2.x版本 |
2000年 | ISAM升级成MyISAM引擎。MySQL开源 |
2003年 | MySQL4.0发布,集成InnoDB存储引擎 |
2005年 | MySQL5.0发布,提供了视图、存储过程等功能 |
2008年 | MySQL AB公司被Sun公司收购,进入Sun MySQL时代 |
2009年 | Oracle收购Sun公司,进入Oracle MySQL时代 |
2010年 | MySQL5.5发布,InnoDB成为默认存储引擎 |
2016年 | MySQL8.0.0版本发布 |
MySQL主要分支:
- MariaDB:Oracle收购MySQL之后,MySQL创始人之一Monty担心MySQL发展的未来(开发缓慢、封闭、可能闭源),就创建了一个分支MariaDB(2009年),默认使用Maria存储引擎,它是原MyISAM存储引擎的升级版本。
- Percona Server:基于InnoDB存储引擎,提升了性能和易管理性,形成了XtraDB引擎。
- 网易的InnoSQL
- 极数据云舟的ArkDB
二、一条SQL语句是如何执行的?
1. 连接
MySQL服务监听的端口默认是3306。
查看当前有多少连接?
show global status like 'Thread%';
字段 | 含义 |
---|---|
Threads_cached | 缓存中的线程连接数 |
Threads_connected | 当前打开的连接数 |
Threads_created | 为处理连接创建的线程数 |
Threads_running | 非睡眠状态的连接数,通常指并发连接数 |
连接超时参数:
show global variables like 'wait_timeout'; -- 非交互超时时间,如JDBC程序
show global variables like 'interactive_timeout'; -- 交互时超时工具,如navicat
默认都是28800秒,8小时。
MySQL允许最大连接数(并发数)为151个,最大可设置为100000.
show variables like 'max_connections';
2,查询缓存
show variables like 'query_cache%';
MySQL的缓存默认是关闭的。不推荐使用,因为它要求SQL语句必须一模一样(多一个空格,字母大小写都不行),还有当表中的数据发生变化时,缓存会失效。 在MySQL8.0中,查询缓存被移除了。
3,词法解析和预处理(Parser & Preprocessor)
- 词法解析:就是把一个完整的SQL语句打碎成一个个的单词。每个符号是什么类型,从哪里开始到哪里结束。
-
语法解析:对SQL进行语法检查,如单引号有没有闭合,根据MySQL的语法规则,生成解析树(select_lex)
任何数据库中间件,要解析SQL完成路由功能,必须要有词法和语法分析功能,如Mycat,Sharding-JDBC等,开源词法解析有LEX、Yacc等。
- 预处理器:检查生成的解析树,解决解析器无法解析的语义。比如检查表和列名是否存在,检查名字和别名,保证没有歧义。
4,查询优化(Query Optimizer)与查询执行计划
查询优化器的目的是根据解析树生成不同的执行计划(Execution Plan),然后选择一种最做优的执行计划,MySQL里使用的是基于开销(cost)的优化器。查看查询的开销:
show status like 'Last_query_cost';
参考书籍:《数据库查询优化器的艺术-原理解析与SQL性能优化》
优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。可以使用 EXPLAIN,可以看到查询执行计划的信息。
EXPLAIN select name from user where id = 1;
如果要得到详细的信息,可以用 FORMAT=JSON,或者开启optimizer trace.
explain FORMAT=JSON select name from user where id = 1;
5,存储引擎
查看数据库存放数据的路径:
show VARIABLES like 'datadir';
默认情况下,每个数据库有一个自己的文件夹,任何一个存储引擎都有一个frm文件,这个是表结构的定义文件。不同的存储引擎存放数据的方式不一样,产生的文件也不一样,innodb是1个,memory没有,myisam有两个。
一张表的存储引擎是在创建表的时候指定的,使用ENGINE关键字,5.5.5之前,默认为MyISAM,5.5.5之后,默认的存储引擎为InnoDB.
常见存储引擎:
https://dev.mysql.com/doc/refman/5.7/en/storage-engines.html
MyISAM:使用范围较小。表级锁定限制了读/写的性能,常用于只读或以读为主的工作。特点:1,支持表级别的锁(插入和更新会锁表)。不支持事务。2,拥有较高的插入和查询速度。3,存储了表的行数(count速度快)。
适合:只读之类的数据分析项目。
如果快速向数据库插入100万数据,先用MyISAM插入,再修改存储引擎为InnoDB.InnoDB:InnoDB是一个事务安全(与ACID兼容)的存储引擎,具有提交、回滚和崩溃恢复功能来保护用户数据。InnoDB行级锁提高了多用户并发性和性能。InnoDB将用户数据存储在聚集索引中,以减少基于主键的常见查询的I/O。为保持数据完整性,InnoDB还支持外健引用完整性约束。
特点:1,支持事务、支持外健,数据完整性、一致性更高。2,支持行级别锁和表级别锁。3,支持读写并发,写不阻塞读(MVCC)。4,特殊的索引存放方式,可减少I/O,提升查询效率。
适合:经常更新的表,存在并发读写或有事务处理的业务系统。Memory:将数据存储在RAM中,以快速查找非关键数据的环境提供了快速访问。特点:数据放在内存里,读写速度快,但数据库重启或崩溃,数据会丢失。只适合做临时表。
选择存储引擎:
对数据一致性要求高,需要事务支持,选择InnoDB。
数据查询多更新少,对查询性能要求较高,选择MyISAM。
用于查询的临时表,选择Memory。
可以根据官网的手册用C语言开发一个存储引擎:https://dev.mysql.com/doc/internals/en/custom-engine.html
show engine innodb status;
6,执行引擎(Query Execution Engine),返回结果
使用存储引擎提供的API来完成相应操作,修改了存储引擎,操作方式不需要改变?因为不同的存储引擎实现的API是相同的。
三,MySQL体系架构
1,连接层:客户端要连接到MySQL服务器的3306端口,要跟服务器建立连接,管理所有的连接,验证客户端的身份和权限,这些功能在连接层完成。
2,服务层:查询缓存的判断、根据SQL调用相应的接口,对SQL进行词法和语法的解析,对SQL进行优化,交给执行器去执行。
3,存储引擎:存数据的地方,支持不同的存储引擎
四,更新SQL是怎么执行的
update操作包含了更新、插入和删除。在MyBatis源码中,Executor里只有doQuery()和doUpdate()方法,没有doDelete()和doInsert()。
1,缓冲池 Buffer Pool
对InnoDB存储引擎来说,要操作数据,要把磁盘的数据加载到内存里才能操作。存储引擎有一个预读取的概念,就是磁盘上的一块数据被读取时,很有可能它附近位置的数据马上被读取到,这个叫局部性原理。
InnoDB设定存储引擎从磁盘读取数据到内存的最小单位,叫做页。默认为16KB。数据所在的页叫数据页。
InnoDB设计了一个内存缓冲区,读数据的时候,先判断是不是在这个内存区域中,如果是直接读取,然后操作,不用从磁盘加载。如果不是,读取之后写到这个内存的缓冲区,这个缓冲区就叫做 Buffer Pool。
修改数据的时候,也是先写到buffer pool,而不是直接写到磁盘。内存中的数据和磁盘数据不一致的时候,叫做脏页。InnoDB有专门的后台线程把Buffer Pool的数据写入到磁盘,每隔一段时间就一次性把多个修改写入磁盘,叫脏刷。
Buffer Pool的作用是提高读写的效率。
2,Redo log
InnoDB把所有页面的修改操作专门写到一个日志文件,如果有未同步到磁盘的数据,数据库启动的时候,会从这个日志文件中进行恢复(实现crash-safe),即事务ACID中的D(持久性),就是用它来实现的。这个日志文件就是磁盘的redo log。
写日志文件和写数据文件有什么区别?
刷盘是随机I/O,而记录日志是顺序I/O,顺序I//O效率更高,本质上是数据集中存储和分散存储的区别。先把修改写到日志文件,在保证内存数据安全性的情况下,可以延迟刷盘时机,进行提升系统吞吐。
redo log位于/var/lib/mysql目录下的ib_logfile0和ib_logfile1,默认2个文件,每个48M。
show variables like 'innodb_log%';
参数 | 含义 |
---|---|
innodb_log_file_size | 指定每个文件的大小,默认48M |
innodb_log_files_in_group | 指定文件的数量,默认为2 |
innodb_log_group_home_dir | 指定文件所在路径,相对或绝对。如果不指定,则为datadir路径。 |
redo log的特点:
- redo log是InnoDB存储引擎实现的,并不是所有存储引擎都有,支持崩溃恢复是InnoDB的一个特性。
- redo log不是记录数据页更新之后的状态,而是记录“在某个数据页上做了什么修改”。属于物理日志。
- redo log大小是固定的,前端的内容会被覆盖,一旦写满,会触发buffer pool到磁盘的同步。
3,Undo log
undo log(撤销或回滚日志)记录了事务发生之前的数据状态,分为insert undo log 和 update undo log。如果修改数据库时出现异常,可用undo log来实现回滚操作(原子性)。
undo log记录的是反向的操作,如insert记录的是delete,update会记录update原来的值。
show global variables like '%undo%';
参数 | 含义 |
---|---|
innodb_max_undo_log_size | 如果innodb_undo_log_truncate设置为1,超过这个大小会触发truncate动作,如果page大小为16KB,truncate 后空间缩小到10M,默认1G |
innodb_undo_directory | undo log文件路径 |
innodb_undo_log_truncate | 设置为1,即开启在线回收undo log日志文件 |
innodb_undo_logs | 回滚段的数量,默认128 |
innodb_undo_tablespaces | 设置undo独立表空间个数,范围了0-95,默认为0,表示不开启独立undo表空间 |
4,更新过程
1,事务开始,从内存(buffer pool)或磁盘(data file)取到包含这条数据的数据页,返回给server执行器。
2,server执行器修改数据页的这一行数据值。
3,记录undo log。
4,记录redo log。
5,调用存储引擎接口,记录数据页到Buffer Pool。
6,事务提交。
5,InnoDB总体架构
1,内存架构
Buffer Pool主要分3个部分:Buffer Pool、Change Buffer、Adaptive Hash Index,还有一个(redo) log buffer。
1.1、Buffer Pool
Buffer Pool缓存的是页面信息,包括数据页、索引页。默认大小为128M,可调整。
show variables like '%innodb_buffer_pool%';
查看服务器状态,有很多和Buffer Pool有关的信息:
show status like '%innodb_buffer_pool%';
可在官网查询详细含义:https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
1.2、LRU
内存缓冲池写满之后,InnoDB用LRU算法来管理缓冲池(链表实现)。
InnoDB使用一个双向链表,LRU list,该list放的不是data page,而是指向缓存页的指针。InnoDB的数据不是都是在访问的时候才缓存到buffer pool中的,InnoDB有一个预读机制(read ahead),设计者认为访问某个page数据的时候,相邻的page可能很快被访问到,所以把这些page放到buffer pool中缓存起来。预读机制分为两种类型,一种叫线性预读(异步的)(Linear read-ahead)。为便于管理,InnoDB把64个相邻的page叫做一个extent(区)。如果顺序访问了一个extent的56个page,这时InnoDB会把下一个extent缓存到buffer pool中。
顺序访问到多少page才缓存下一个extent,由一个参数控制:
show variables like 'innodb_read_ahead_threshold';
第二种叫随机预读(Random read-ahead),如果buffer pool已经缓存了同个extent的数据页超过13个,就会把这个extent剩余的page全部缓存到buffer pool。
随机预读的功能默认不启用,由参数控制:
show variables like 'innodb_random_read_ahead';
所有新数据加入buffer pool的时候,一律放到冷区的head,不管是预读的,还是普通的读操作。如果再次被访问,就移动到热区的head。热区的数据长时间没有被访问,先移动到冷区的head,慢慢在tail被淘汰。
默认情况下,热区占了5/8的大小,冷区占了3/8。由innodb_old_blocks_pct控制。
对于加载到冷区的数据,设置一个时间窗口,只有超过这个时间之后被访问,才认为是有效的访问,通过innodb_old_blocks_time参数控制,默认1秒钟。从而解决全表描述或者预读的数据污染热数据。
1.3、Change Buffer 写缓冲
如果这个数据页不是唯一索引,不存在数据重复的情况,可以先把修改记录在内存的缓冲池中,从而提升更新语句的执行速度。这一区域叫Change Buffer。把Change Buffer的数据记录到数据页的操作叫merge。在访问这个数据页的时候,或者通过后台线程,或者数据库shut down,redo log写满的时候触发。
如果数据库大部分索引是非唯一索引,并且业务写多读少,不会在写数据后立刻读取,可以使用Change Buffer(写缓冲)。
show variables like 'innodb_change_buffer_max_size';
代表change buffer占buffer pool的比例,默认为25%。
1.4、Adaptive Hash Index
哈希索引,见下节。
1.5、Redo log Buffer
Redo log也不是每一次都写入磁盘,在Buffer Pool里有一块内存区域(Log Buffer)用来保存将要写入日志文件的数据,默认16M,可节省磁盘IO。
show variables like 'innodb_log_buffer_size';
redo log主要用于崩溃恢复。redo log写入磁盘,不是写入数据文件。
log buffer写入磁盘的时机,由一个参数控制,默认为1。
show variables like 'innodb_flush_log_at_trx_commit';
值 | 含义 |
---|---|
0(延迟写) | log buffer每秒一次地写入log file中,并且log file的flush操作同时进行。该模式下,在事务提交的时候,不会主动触发写入磁盘的操作 |
1(默认,实时写,实时刷) | 每次事务提交时都会把log buffer的数据写入log file,并且刷到硬盘中去 |
2(实时写,延时刷) | 每次事务提交时,会把log buffer的数据写入log file。但是flush操作不会同时进行。该模式下,MySQL每秒执行一次flush操作 |
1.3 磁盘结构
表空间是InnoDB存储引擎结构的最高层,所有的数据都保存在表空间中。分为5类。
- 系统表空间(system tablespace):默认情况下InnoDB存储引擎有一个共享表空间(/var/lib/mysql/ibdata1),叫系统表空间。
包含InnoDB的数据字典和双写缓冲区,Change Buffer和Undo Logs,如果没有指定file-per-table,也包含用户创建的表和索引数据。- undo在后面介绍,因为可以设置独立的表空间
- 数据字典:由内部系统表组成,存储表和索引的元数据。
- 双写缓冲。
如果存储引擎在写页的数据时发生了宕机,可能出现页只写了一部分的情况,这种情况叫做写失效(partial page write),可能导致数据丢失。这时页本身已经损坏,无法进行崩溃恢复。所以在应用redo log之前,需要一个页的副本。如果出现了写失效,就用页的副本来还原这个页,然后再用redo log。这个页的副本就是double write。
- 独占表空间 file-per-table tablespace:可以让每张表占一个表空间,通过innodb_file_per_table配置,默认开启。
show variables like 'innodb_file_per_table';
开启后,每张表会开启一个表空间,这个文件就是数据目录下的idb文件,用来存放表的索引和数据。
- 通用表空间 general tablespace:可能创建一个能用表空间,用来存放不同数据库的表,数据路径和文件可以自定义。语法:
create tablespace ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb;
在创建表的时候可以指定表空间,用ALERT修改表空间可以转换表空间。
create table t2673(id integer) tablespace ts2673;
不同表空间的数据是可以移动的。删除表空间需要先删除里面的所有表。
drop table t2673;
drop tablespace ts2673;
- 临时表空间 temporary tablespaces
存储临时表的数据,包括用户创建的临时表,和磁盘内部的临时表。对应数据目录下的ibtmp1文件。当服务器正常关闭吧,该表空间被删除。
6,后台进程
后台线程主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为:master thread,IO thread,purge thread,page cleaner thread。
- master thread :负责刷新缓存数据到磁盘并协调调度其它后台进程。
- IO thread:分为insert buffer、log、read、write进程。分别用来处理insert buffer、重写日志、读写请求的IO回调。
- purge thread:用来回收undo页。
- page cleaner thread:用来刷新脏页。
7,Binlog
binlog以事件的形式记录了所有的DDL和DML语句(记录的是操作,属于逻辑日志),可以用来做主从复制和数据恢复。
跟redo log不一样,它的文件内容是可以追加的,没有固定大小限制。
在开启binlog功能的前提下,可以把binlog导出成SQL语句,把所有的操作重放一遍,来实现数据的恢复。
binlog的另一个功能是实现主从复制,它的原理是从服务器读取主服务器的binlog,然后执行一遍。
为什么需要两阶段提交?
如果在执行更新操作的时候,如果写完redo log,还没有写binlog的时候,MySQL重启了。因为redo log用于在重启的时候恢复数据,所以会执行更新,但是binlog没有记录这个逻辑日志,这时候用binlog去恢复数据或者同步到从库,会出现数据不一致的情况。所以在写两个日志的情况下,binlog充当事务的协调者。通知InnoDB来执行prepare或者commit或者rollback。如果6步写binlog失败,就不会提交。