MySQL是一种开放源代码的关系型数据库管理系统(RDBMS),使用最常用的数据库管理语言–结构化查询语言(SQL:Structured Query Language)进行数据库管理。
MySQL的发展历史和版本分支:
时间 | 里程碑 |
---|---|
1996 年 | MySQL 1.0 发布。它的历史可以追溯到 1979 年,作者 Monty 用 BASIC 设计 的一个报表工具。 |
1996 年10月 | 3.11.1发布: MySQL没有2.x版本。 |
2000 年 | ISAM升级成MylSAM引擎,MySQL开源。 |
2003 年 | MySQL 4.0发布,集成InnoDB存储引擎。 |
2005 年 | MySQL 5.0 版本发布,提供了视图、存储过程等功能。 |
2008 年 | MySQL AB公司被Sun公司收购,进入Sun MySQL时代。 |
2009 年 | Oracle收购Sun公司,进入Oracle MySQL时代。 |
2010 年 | MySQL 5.5发布,InnoDB成为默认的存储引擎。 |
2016 年 | MySQL发布8.0.0版本。为什么没有6、7? 5.6可以当成6.x,5.7可以当成 7.X。 |
MySQL 使用典型的客户端/服务器(Client/Server)结构。
Connector(连接器)
用来支持各种语言和 SQL 的交互,比如 PHP,Python,Java 的 JDBC。
Management Serveices & Utilities(系统管理和控制工具)
包括备份恢复、MySQL 复制、集群等等。
Connection Pool(连接池)
管理需要缓冲的资源,包括用户密码、权限线程等等。
SQL Interface(SQL接口)
用来接收用户的 SQL 命令,返回用户需要的查询结果。
Parser(解析器)
用来解析 SQL 语句。
Optimizer(查询优化器)
利用数据库的统计信息决定 SQL 语句的最佳执行方式。
使用索引还是全表扫描的方式访问单个表,多表连接的实现方式等。
优化器是决定查询性能的关键组件,而数据库的统计信息是优化器判断的基础。
Cache and Buffer(缓存)
由一系列缓存组成的,例如数据缓存、索引缓存以及对象权限缓存等。
对于已经访问过的磁盘数据,在缓冲区中进行缓存;下次访问时可以直接读取内存中的数据,从而减少磁盘 IO。
Pluggable Storage Engines(可插拔存储引擎)
它提供 API 给服务层使用,跟具体的文件打交道。
跟客户端对接的连接层:
客户端要连接到 MySQL 服务器 3306 端口,必须要跟服务端建立连接,那么 管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。
真正执行操作的服务层:
连接层会把 SQL 语句交给服务层,这里面又包含一系列的流程:
比如查询缓存的判断、根据 SQL 调用相应的接口,对我们的 SQL 语句进行词法和语法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。
然后就是优化器,MySQL 底层会根据一定的规则对我们的 SQL 语句进行优化,最后再交给执行器去执行。
和跟硬件打交道的存储引擎层:
存储引擎是数据真正存放的地方,在 MySQL 里面支持不同的存储引擎。
再往下就是内存或者磁盘。
执行流程图如下:
当向MySQL发送一条SQL请求的时候,MySQL进行了如下步骤:
客户端与服务器建立连接,发送语句;
服务器先查询缓存,如果命中了缓存,则立刻返回缓存中的结果;
如果没有缓存,则服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划;
MySQL根据优化器生成的执行计划,调用存储引擎的API来执行;
将结果返回给客户端。
在开发系统跟第三方对接的时候,需要确认两项:
通信协议
是用 HTTP 还是 TCP 或者 WebService。
消息格式
是用 XML 格式,还是 JSON 格式。
MySQL 支持多种通信协议,可以使用同步/异步的方式,也支持长连接/短连接。
Unix Socket
在 Linux 服务器上,如果没有指定-h 参数,就是用 socket 的方式登录的。
它不用通过网络协议,也可以连接到 MySQL 的服务器,它需要用到服务器上的一个物理文件(/var/lib/mysql/mysql.sock)。
TCP/IP 协议
如果指定-h 参数,就会用该方式。
编程语言的连接模块都是用 TCP 协议连接到 MySQL 服务器的。
命名管道(Named Pipes)和内存共享(Share Memory)
这两种通信方式只能在 Windows 上面使用,一般用得比较少。
单工
数据的传输是单向的。生活中的类比-遥控器。
半双工
数据传输是双向的,在这个通讯连接里面,同一时间只能有一台服务器在发送数据,也就是你要给我发的话,也必须等我发给你完了之后才能给我发。生活中的类比-对讲机。
全双工
数据的传输是双向的,并且可以同时传输。生活中的类比-打电话。
要么是客户端向服务端发送数据,要么是服务端向客户端发送数据,这两个动作不能同时发生。所以客户端发送 SQL 语句给服务端的时候,(在一次连接里面)数据是不能分成小块发送的,不管你的 SQL 语句有多大,都是一次性发送。
比如用 MyBatis 动态 SQL 生成了一个批量插入的语句,插入 10 万条数据,values 后面跟了一长串的内容,或者 where 条件 in 里面的值太多,就会出现问题。
这个时候必须要调整 MySQL 服务器配置 max_allowed_packet 参数的值(默认是 4M),把它调大,否则就会报错。
另一方面,对于服务端来说,也是一次性发送所有的数据,不能因为已经取到了想要的数据就中断操作,这个时候会对网络和内存产生大量消耗。
所以,一定要在程序里面带上 limit 操作。
比如一次性把所有满足条件的数据全部查出来,一定要先 count 一下。如果数据量很大的话,可以分批查询。
同步
依赖于被调用方,受限于被调用方的性能。也就是说,应用操作数据库, 线程会阻塞,等待数据库的返回;
一般只能做到一对一,很难做到一对多的通信。
异步
异步可以避免应用阻塞等待,但不能节省 SQL 执行的时间;
如果异步存在并发,每一个 SQL 的执行都要单独建立一个连接,避免数据混乱。
但这样会给服务端带来巨大的压力,一个连接就会创建一个线程,线程间切换会占用大量 CPU 资源。另外异步通信还带来了编码的复杂度,所以一般不建议使用。如果要异步,必须使用连接池,排队从连接池获取连接而不是创建新连接。
一般来说MySQL会在连接池中使用长连接
MySQL 既支持短连接,也支持长连接。短连接就是操作完毕以后,马上会关闭。长连接可以保持打开,减少服务端创建和释放连接的消耗,后面的程序访问的时候还可以使用这个连接。
保持长连接会消耗内存。长时间不活动的连接,MySQL 服务器也会断开。
默认都是 28800 秒,8 小时。
查看 MySQL 当前连接数
-
Threads_cached:缓存中的线程连接数。
Threads_connected:当前打开的连接数。
Threads_created:为处理连接创建的线程数。
Threads_running:非睡眠状态的连接数,通常指并发连接数。每产生一个连接或者一个会话,在服务端就会创建一个线程来处理。反过来,如果关闭了会话,也就杀掉了线程。
状态 | 含义 |
---|---|
Sleep | 线程正在等待客户端,以向它发送一个新语句 |
Query | 线程正在执行查询或往客户端发送数据 |
Locked | 该查询被其它查询锁定 |
Copying to tmp table on disk | 临时结果集合大于 tmp_table_size。线程把临时表从存储器内部格式改变为磁盘模式,以节约存储器 |
Sending data | 线程正在为 SELECT 语句处理行,同时正在向客户端发送数据 |
Sorting for group | 线程正在进行分类,以满足 GROUP BY 要求 |
Sorting for order | 线程正在进行分类,以满足 ORDER BY 要求 |
MySQL 服务允许的最大连接数
在 5.7 版本中默认是 151 个,最大可以设置成 16384(2^14)。
show 语句的参数说明:
当前会话为session级别(默认),全局会话为global级别。
set动态修改,重启后会失效,要想永久生效,需修改配置文件/etc/my.cnf。
修改全局最大连接数:
set global max_connections = 2000;
MySQL 内部自带了一个缓存模块。
把数据以 KV 的形式放到内存里面,加快数据的读取速度,也可以减少服务器处理的时间。
MySQL 的缓存默认是关闭的
为什么 MySQL 不推荐使用自带的缓存呢?
主要是因为 MySQL 自带的缓存的应用场景有限且应用条件严格。
第一:它要求 SQL 语句必须一模一样,中间多一个空格,字母大小写不同都被认为是不同的的 SQL。
第二:表里面任何一条数据发生变化的时候,这张表所有缓存都会失效,所以对 于有大量数据更新的应用,也不适合。
所以缓存这一块,还是交给 ORM 框架(MyBatis 默认开启了一级缓存), 或者独立的缓存服务,如 Redis 来处理更合适。
在 MySQL 8.0 中,查询缓存已经被移除了。
为什么一条 SQL 语句能够被服务端识别呢?它是怎么知道输入的内容是错误的呢?
就是因为 MySQL 的解析器和预处理模块在行使作用。
这一步会对语句基于 SQL 语法进行词法和语法分析和语义的解析。
词法解析
词法分析就是把一个完整的 SQL 语句打碎成一个个的单词。
比如一个简单的 SQL 语句:select * from user where id = 1;
它会打碎成 8 个符号,每个符号是什么类型,从哪里开始到哪里结束。
语法解析
语法分析会对 SQL 做一些语法检查,然后根据 MySQL 定义的语法规则,将SQL 语句生成一个数据结构。这个数据结构我们把它叫做解析树(select_lex)。
任何数据库的中间件,比如 Mycat,Sharding-JDBC,都必须要有词法和语法分析功能,在市面上也有很多的开源的词法解析的工具(比如 LEX,Yacc)。
预处理器
解析器可以分析语法,但是它如何知道数据库里面有什么表,表里面有什么字段呢?
如果写了一个词法和语法都正确的 SQL,但是表名或者字段不存在,会在数据库的执行层还是解析器报错?
答案是会在解析的时候报错。
解析 SQL 的环节里面有个预处理器。它会检查生成的解析树,解决解析器无法解析的语义。如它会检查表和列名是否存在,检查名字和别名,保证没有歧义。
预处理之后会得到一个新的解析树。
一条 SQL 语句是不是只有一种执行方式?或者说数据库最终执行的 SQL 是不是就是我们发送的 SQL?
其实一条 SQL 语句是可以有很多种执行方式的,最终返回相同的结果,他们是等价的。
但是如果有这么多种执行方式,这些执行方式怎么得到的?最终选择哪一种去执行?根据什么判断标准去选择?
这个就是 MySQL 的查询优化器(Optimizer)要做的事情。
查询优化器的目的是根据解析树生成不同的执行计划(Execution Plan),然后选择一种最优的执行计划。
MySQL 里面使用的是基于开销(cost)的优化器,哪种执行计划开销最小,就用哪种。
使用命令查看查询的开销:show status like ‘Last_query_cost’;
优化器能处理的优化类型
子查询优化
等价谓词重写
条件化简
外连接消除
嵌套连接消除
连接的消除
语义优化
非SPJ优化
优化器也不是万能的,并不是再垃圾的 SQL 语句都能自动优化,也不是每次都能选择到最优的执行计划,因此,在编写 SQL 语句的时候还是要注意的。
优化器获取执行计划
首先要启用优化器的trace工具(默认是关闭的)
查看TRACE字段内容:
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `u`.`id` AS `id`,`u`.`name` AS `name`,`u`.`phone` AS `phone`,`u`.`age` AS `age`,`u`.`create_time` AS `create_time`,`u`.`update_time` AS `update_time` from (`user` `u` join `user_skill` `us` on((`u`.`id` = `us`.`user_id`)))"
},
{
"transformations_to_nested_joins": {
"transformations": [
"JOIN_condition_to_WHERE",
"parenthesis_removal"
],
"expanded_query": "/* select#1 */ select `u`.`id` AS `id`,`u`.`name` AS `name`,`u`.`phone` AS `phone`,`u`.`age` AS `age`,`u`.`create_time` AS `create_time`,`u`.`update_time` AS `update_time` from `user` `u` join `user_skill` `us` where (`u`.`id` = `us`.`user_id`)"
}
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`u`.`id` = `us`.`user_id`)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "multiple equal(`u`.`id`, `us`.`user_id`)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "multiple equal(`u`.`id`, `us`.`user_id`)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal(`u`.`id`, `us`.`user_id`)"
}
]
}
},
{
"substitute_generated_columns": {
}
},
{
"table_dependencies": [
{
"table": "`user` `u`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
]
},
{
"table": "`user_skill` `us`",
"row_may_be_null": false,
"map_bit": 1,
"depends_on_map_bits": [
]
}
]
},
{
"ref_optimizer_key_uses": [
{
"table": "`user` `u`",
"field": "id",
"equals": "`us`.`user_id`",
"null_rejecting": false
},
{
"table": "`user_skill` `us`",
"field": "user_id",
"equals": "`u`.`id`",
"null_rejecting": false
}
]
},
{
"rows_estimation": [
{
"table": "`user` `u`",
"table_scan": {
"rows": 4,
"cost": 1
}
},
{
"table": "`user_skill` `us`",
"table_scan": {
"rows": 6,
"cost": 1
}
}
]
},
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`user` `u`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "PRIMARY",
"usable": false,
"chosen": false
},
{
"rows_to_scan": 4,
"access_type": "scan",
"resulting_rows": 4,
"cost": 1.8,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 4,
"cost_for_plan": 1.8,
"rest_of_plan": [
{
"plan_prefix": [
"`user` `u`"
],
"table": "`user_skill` `us`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "idx_user_third",
"rows": 1.5,
"cost": 5.2041,
"chosen": true
},
{
"access_type": "scan",
"chosen": false,
"cause": "covering_index_better_than_full_scan"
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 6,
"cost_for_plan": 7.0041,
"chosen": true
}
]
},
{
"plan_prefix": [
],
"table": "`user_skill` `us`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "idx_user_third",
"usable": false,
"chosen": false
},
{
"rows_to_scan": 6,
"access_type": "scan",
"resulting_rows": 6,
"cost": 2.2,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 6,
"cost_for_plan": 2.2,
"rest_of_plan": [
{
"plan_prefix": [
"`user_skill` `us`"
],
"table": "`user` `u`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "eq_ref",
"index": "PRIMARY",
"rows": 1,
"cost": 7.2,
"chosen": true,
"cause": "clustered_pk_chosen_by_heuristics"
},
{
"rows_to_scan": 4,
"access_type": "scan",
"using_join_cache": true,
"buffers_needed": 1,
"resulting_rows": 4,
"cost": 5.8001,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 24,
"cost_for_plan": 8.0001,
"pruned_by_cost": true
}
]
}
]
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`us`.`user_id` = `u`.`id`)",
"attached_conditions_computation": [
],
"attached_conditions_summary": [
{
"table": "`user` `u`",
"attached": null
},
{
"table": "`user_skill` `us`",
"attached": null
}
]
}
},
{
"refine_plan": [
{
"table": "`user` `u`"
},
{
"table": "`user_skill` `us`"
}
]
}
]
}
},
{
"join_execution": {
"select#": 1,
"steps": [
]
}
}
]
}
它是一个 JSON 类型的数据。
主要分成三部分,准备阶段、优化阶段和执行阶段。
expanded_query 是优化后的 SQL 语句。considered_execution_plans 里面列出了所有的执行计划。
最后,分析完关掉trace工具。
set optimizer_trace="enabled=off";
SHOW VARIABLES LIKE 'optimizer_trace';
优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。
当然,这个执行计划不一定就是最优的,因为 MySQL 也有可能覆盖不到所有的执行计划。
MySQL 提供了查看一个执行计划的工具。在 SQL 语句前面加上 EXPLAIN,就可以看到执行计划的信息。
EXPLAIN select * from user where id = 1;
注意:Explain 的结果也不一定最终执行的方式。
执行引擎通过使用执行计划去操作存储引擎。利用存储引擎提供的相应 API 来完成SQL操作。
为什么修改了表的存储引擎,操作方式不需要做任何改变?
因为不同功能的存储引擎实现的 API 是相同的。最后把数据返回给客户端,即使没有结果也要返回。
存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用存储引擎进行创建、查询、更新和删除数据。
不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。
在关系数据库中数据的存储是以表的形式存储的,表在存储数据的同时,还要组织数据的存储结构,这个存储结构就是由存储引擎决定的,所以存储引擎也可以称为表类型(Table Type,即存储和操作此表的类型)。
从逻辑的角度来说,数据是存放在存储引擎这种结构中。
在 MySQL 里面,支持多种存储引擎,是可以替换的,所以叫做插件式的存储引擎。
数据是如何存储在磁盘
可通过以下语句找到数据库存放数据的磁盘路径:
show variables like ‘datadir’;
默认情况下,每个数据库都对应一个文件夹。
任何一个存储引擎都有一个 frm 文件,这个是表结构定义文件。
不同的存储引擎存放数据的方式不一样,产生的文件也不一样,innodb 是 1 个, memory 没有,myisam 是两个。
存储引擎比较
MyISAM 和 InnoDB 是用得最多的两个存储引擎,在 MySQL 5.5 版本之前, 默认的存储引擎是 MyISAM,它是 MySQL 自带的。创建表的时候不指定存储引擎, 它就会使用 MyISAM 作为存储引擎。
MyISAM 的前身是 ISAM(Indexed Sequential Access Method:利用索引,顺序存取数据的方法)。5.5 版本之后默认的存储引擎改成了 InnoDB,它是第三方公司为 MySQL 开发的。
为什么要改呢?
使用innoDB最主要的原因还是 InnoDB 支持事务,支持行级别的锁,对于业务一致性要求高的场景来说更适合。
MyISAM(3 个文件):
应用范围比较小。
表级锁定限制了读/写的性能,因此在 Web 和数据仓库配置中, 它通常用于只读或以读为主的工作。
特点:
支持表级别的锁(插入和更新会锁表);
不支持事务;
拥有较高的插入(insert)和查询(select)速度;
存储了表的行数(count 速度更快)。
InnoDB(2 个文件):
mysql 5.7 中的默认存储引擎。
InnoDB 是一个事务安全(与 ACID 兼容)的 MySQL 存储引擎,它具有提交、回滚和崩溃恢复功能来保护用户数据。
InnoDB 行级锁(不升级 为更粗粒度的锁)和 Oracle 风格的一致非锁读提高了多用户并发性和性能。
InnoDB 将用户数据存储在聚集索引中,以减少基于主键的常见查询的 I/O。
为了保持数据完整性,InnoDB 还支持外键引用完整性约束。
适合:经常更新的表,存在并发读写或者有事务处理的业务系统。
特点:
支持事务,支持外键,因此数据的完整性、一致性更高;
支持行级别的锁和表级别的锁;
支持读写并发,写不阻塞读(MVCC);
特殊的索引存放方式,可以减少 IO,提升查询效率。
Memory(1 个文件):
将所有数据存储在 RAM 中,以便在需要快速查找非关键数据的环境中快速访问。这个引擎以前被称为堆引擎。其使用案例正在减少。InnoDB 及其缓冲池内存区域提供了一 种通用、持久的方法来将大部分或所有数据保存在内存中,而 ndbcluster 为大型分布式数据集提供了快速的键值查找。
特点:
把数据放在内存里面,读写的速度很快,但是数据库重启或者崩溃,数据会全部消 失。只适合做临时表。
CSV(3 个文件):
它的表实际上是带有逗号分隔值的文本文件。
csv 表允许以 csv 格式导入或转储数据, 以便与读写相同格式的脚本和应用程序交换数据。
因为 csv 表没有索引,所以通常在正常操作期间将数据保存在 innodb 表中,并且只在导入或导出阶段使用 csv 表。
适合:在不同数据库之间导入导出。
特点:
不允许空行,不支持索引。格式通用,可以直接编辑。
Archive(2 个文件):
用于存储和检索大量很少引用的历史、存档或安全审计信息。
特点:
不支持索引,不支持 update、delete。
总结
我们看到了,不同的存储引擎提供的特性都不一样,它们有不同的存储机制、索引方式、锁定水平等功能。
在不同的业务场景中对数据操作的要求不同,就可以选择不同的存储引擎来满足我们的需求,这个就是 MySQL 支持这么多存储引擎的原因。
如果对数据一致性要求比较高,需要事务支持,可以选择 InnoDB。 (正在进行的流水表)
如果数据查询多更新少,对查询性能要求比较高,可以选择 MyISAM。(历史流水表)
如果需要一个用于查询的临时表,可以选择 Memory。
在数据库里面,update 操作其实包括了删除、更新、插入。
更新流程和查询流程基本流程是一致的,区别在于拿到符合条件的数据之后的操作。
InnnoDB 的数据都是放在磁盘上的,InnoDB操作数据有一个最小的逻辑单位,叫做页(索引页和数据页)。
对于数据的操作,并不是每次直接操作磁盘,因为磁盘的速度太慢了。
InnoDB 使用了一种缓冲池的技术,把磁盘读到的页放到一块内存区域里面。这个内存区域就叫 Buffer Pool。
下一次读取相同的页,先判断是不是在缓冲池里面。如果是,就直接读取,不用再 次访问磁盘。
修改数据的时候,先修改缓冲池里面的页。内存的数据页和磁盘数据不一致的时候, 把它叫做脏页。
InnoDB 里面有专门的后台线程把缓冲池的数据写入到磁盘, 每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。
Buffer Pool 是 InnoDB 里面非常重要的一个结构,它的内部又分成几块区域。
往下详细阐述InnoDB 的内存结构和磁盘结构。
Buffer Pool(缓冲池)
Buffer Pool 缓存的是页信息,包括数据页、索引页。
为了提高大容量读取操作的效率,Buffer Pool被分成可以容纳多行的Page(默认16K)。Buffer Pool的底层数据结构是链表,以此管理页。
Buffer Pool 默认大小是 128M(134217728 字节),可以调整。
Buffer Pool LRU:
内存的缓冲池写满了怎么办?
InnoDB 用 LRU 算法来管理缓冲池(链表实现,不是传统的 LRU),经过淘汰的数据就是热点数据。
Buffer Pool的LRU是一种变体。插入数据时,采用了中间策略。中间策略将Buffer Pool视为两个子列表:
new sublist:存放最近访问的子列表
old sublist:存放最近访问较少的子列表
当需要更新一个数据页时,如果数据页在 Buffer Pool 中存在,那么直接更新好了。否则的话,就需要从磁盘加载到内存,再对内存的数据页进行操作。也就是说,如果没有命中缓冲池,至少要产生一次磁盘 IO。
那么,有没有优化的方式呢?
可通过Change Buffer来优化处理。
Change Buffer(变更缓冲区)
当需要更新一个数据页时,如果数据页在内存中,会直接更新。如果这个数据页没有在内存中的话,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读取这个数据页了。
唯一索引(非主键索引)的更新不能使用change buffer,只有普通索引适用。
当Change Buffer里的Page被读的时候,才会被合并到Buffer Pool中。当脏页超过一定比例时,会将其flush到磁盘中。
在内存中,Change Buffer属于Buffer Pool的一部分。
在磁盘上,Change Buffer属于系统表空间一部分。
需要说明的是,change buffer实际上是可以持久化的数据。也就是说,change buffer在内存中有拷贝,也会被写入到磁盘上。
将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。
什么时候发生 merge?
在访问这个数据页的时候;
通过后台线程;
数据库 shut down;
redo log 写满时。
change buffer用的是buffer pool里的内存,因此不能无限增大。
change buffer的大小,可以通过参数innodb_change_buffer_max_size动态设置。
Change Buffer 占 Buffer Pool 内存的比例为 25% (默认)。
Change Buffer优点:
原本修改Buffer Pool不存在的Page需要先从磁盘读取(一次IO操作)到内存中,然后再写入到redo log。
引入Change Buffer后,会先缓存在Change Buffer中,然后再写入到redo log。
由于Change Buffer的存在,避免了从磁盘读取辅助索引到缓冲池所需的大量随机访问IO。
辅助索引不支持Change Buffer的情况:
辅助索引包含降序索引列;
主键包含降序索引。
注意:降序索引在8.0以上版本才支持。
适用场景:写多读少
如果数据库大部分索引都是辅助索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用 Change Buffer(写缓冲)。在一个数据页做merge之前,change buffer记录的变更越多,收益就越大。这种业务模型常见的有账单类、日志类的系统。
写少读多的情况下:change buffer的利用率不高,因为可能刚一更新完可能就触发merge操作,change buffer没有起到减少随机IO,还多了一个维护change buffer的成本。
唯一索引(非主键索引)和普通索引的数据处理区别:
命中唯一索引和普通索引时,MySQL筛选数据的逻辑是不同的。
以下面的查询语句为例做说明:select * from user where name = ‘fussen’;
先通过B+树从根节点开始,按层搜索到叶子节点,也就是数据页。
唯一索引name:索引定义了唯一性,所以在数据页中找到第一个满足条件的记录后,直接返回。
普通索引name:没有定义索引的唯一性,所以在查找到第一条数据后,还会继续遍历下一条,直到查到的name != ‘fussen’ 的时候,结束遍历。
那么,两种索引的查询效率对比如何呢?
差距很小,几乎可忽略。
普通索引相对于唯一索引多了一步操作就是"查找并判断下一条记录",因为都存在于同一个数据页,所以内存遍历对于CPU的开销可忽略。
一个数据页可以存在上千个key,刚好下一条数据在下一个数据页而触发再次读取数据页的概率很低。所以这两种索引的查询效率几乎一样。
当进行增删改操作时,两种索引的效率对比又是如何的呢?
差距明显,使用普通索引效率更高。
对于唯一索引来说,所有的更新操作都要先判断是否违反唯一性约束,而判断操作必须要将数据页读入内存中才能进行。如果都已经读入到内存了,那么直接更新内存会更快,就没必要使用change buffer了。
所以唯一索引使用change buffer 的话反倒会降低效率,只有普通索引能使用到change buffer。
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。change buffer因为减少了随机磁盘访问,所以对更新性能的提升会很明显。
如果业务代码已经保证了不会写入重复数据的某个数据库字段,从性能的角度考虑,选择唯一索引还是普通索引呢?
尽量选择普通索引。
如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭change buffer。而在其他情况下,change buffer都能提升更新性能。
在实际使用中,普通索引和change buffer的配合使用,对于数据量大的表的更新优化还是很明显的。特别地,在使用机械硬盘时,change buffer这个机制的收效是非常显著的。
所以,当你有一个类似“历史数据”的库,并且出于成本考虑用的是机械硬盘时,那你应该特别关注这些表里的索引,尽量使用普通索引,然后把change buffer 尽量开大,以确保这个“历史数据”表的数据写入速度。
Adaptive Hash Index(自适应哈希索引)
Adaptive Hash Index对InnoDB在Buffer Pool中的查询有很大的优化。
针对Buffer Pool中热点页数据,构建索引,一般使用索引键的前缀构建哈希索引。
因为HASH索引的等值查询效率远高于B+ Tree,所以当查询命中hash,就能很快返回结果,不用再去遍历B+ Tree。
Log Buffer(日志缓冲区)
为了避免 Buffer Pool 里面的脏页还没有刷入磁盘时,数据库宕机或者重启,造成数据丢失(如果写操作写到一半,甚至可能会破坏数据文件导致数据库不可用),InnoDB 会把所有对页的修改操作专门写入一个日志文件。然后在数据库启动时,从这个文件进行恢复操作(实现 crash-safe),用它来实现事务的持久性。
这个文件就是磁盘的redo log(重做日志),对应于/var/lib/mysql/目录下的 ib_logfile0 和 ib_logfile1,每个文件容量大小为 48M。
redo log 并不是每一次都直接写入磁盘,在 Buffer Pool 里面有一块内存区域 (Log Buffer)专门用来保存即将要写入日志文件的数据,默认 16M,它一样可以节省磁盘 IO。
日志和磁盘配合的整个过程,就是MySQL里的WAL技术(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。
磁盘的最小组成单元是扇区,通常是 512 个字节。
操作系统和内存打交道,最小的单位是页-Page。操作系统和磁盘打交道,读写磁盘,最小的单位是块-Block。
如果我们所需要的数据是随机分散在不同页的不同扇区中,那么找到相应的数据需 要等到磁臂旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,依次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。
假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。
同样是写磁盘,为什么不直接写到 db 文件里面去?为什么先写日志再写磁盘?
因为刷盘是随机 I/O,而记录日志是顺序 I/O,顺序 I/O 效率更高。因此,先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐。
那么,Log Buffer 什么时候写入 log file?
在写入数据到磁盘的时候,操作系统本身是有缓存的。flush 就是把操作系统缓冲区写入到磁盘。
log buffer 写入磁盘的时机,由一个参数控制,默认是 1。
值 | 含义 |
---|---|
0(延迟写) | log buffer 将每秒一次地写入 log file 中,并且 log file 的 flush 操作同时进行。该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。 |
1(默认,实时写,实时刷) | 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file,并且刷到磁盘中去。 |
2(实时写,延迟刷) | 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file。但是 flush 操作并不会同时进行。该模式下,MySQL 会每秒执行一次 flush 操作。 |
redo log 有什么特点?
redo log 是 InnoDB 存储引擎实现的,并不是所有存储引擎都有。
不是记录数据页更新之后的状态,而是记录这个页做了什么改动,属于物理日志。
redo log 的大小是固定的,前面的内容会被覆盖。
change buffer 和 redo log 的区别
redo log是减少随机写磁盘IO的消耗(转成顺序写)。每个操作先记录redo log,系统空闲时或redo log满时进行磁盘IO。
change buffer是减少随机读磁盘IO 的消耗。更新时如果内存中不存在该数据页,也不需要马上进行磁盘IO,而是先记录在change buffer中,等待时机统一merge,批量更新。
共同工作流程
更新数据;
在内存中,直接更新内存;
没有在内存中,就在内存的 change buffer 区域记录下更新操作这个信息。
将更新操作记入 redo log 中;
事务完成。
执行这条更新语句的成本很低,就是写了两次内存,然后写了一次磁盘,而且还是顺序写的。
磁盘结构里面主要是各种各样的表空间,叫做 Table space。
表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空 间中。
系统表空间(system tablespace)
在默认情况下 InnoDB 存储引擎有一个共享表空间(对应文件/var/lib/mysql/ ibdata1),也叫系统表空间。
InnoDB 系统表空间包含 InnoDB 数据字典、双写缓冲区,Change Buffer 和 Undo logs,如果没有指定 file-per-table,也包含用户创建的表和索引数据。
数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。
双写缓冲(InnoDB 的一大特性):
InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为 16K,操作系统页大小为 4K,InnoDB 的页写入到磁盘时,一个页需要分 4 次写。
查看双写缓冲开启与否状态:show variables like ‘innodb_doublewrite’;
如果存储引擎正在写入页的数据到磁盘时发生了宕机,可能出现页只写了一部分的情况,比如只写了4K,就宕机了,这种情况叫做部分写失效(partial page write),可能会导致数据丢失。
那么如何解决部分写失效的问题呢?
如果页本身已经损坏了,就无法用redo log做崩溃恢复。因此,在应用 redo log 之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再应用 redo log。这个页的副本就是 double write,InnoDB 的双写技术。通过它实现了数据页的可靠性。
跟 redo log 一样,double write 由两部分组成,一部分是内存的 double write,一个部分是磁盘上的 double write。因为 double write 是顺序写入的,不会带来很大的开销。
在默认情况下,所有的表共享一个系统表空间,这个文件会越来越大,而且它的空 间不会收缩。
独占表空间(file-per-table tablespaces)
可以让每张表独占一个表空间。
通过 innodb_file_per_table 设置,默认开启。
开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的 ibd 文件(例如 /var/lib/mysql/gupao/user_innodb.ibd),存放表的索引和数据。
但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次 写缓冲(Double write buffer)等还是存放在原来的共享表空间内。
通用表空间(general tablespaces)
通用表空间也是一种共享的表空间,跟系统表空间类似。
可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义。
create tablespace ts add datafile '/var/lib/mysql/ts.ibd' file_block_size=16K engine=innodb;
在创建表的时候可以指定表空间,用 ALTER 修改表空间可以转移表空间。
create table user(id int) tablespace ts;
不同表空间的数据是可以移动的。
删除表空间需要先删除里面的所有表。
drop table user;
drop tablespace ts;
临时表空间(temporary tablespaces)
存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。
undo log tablespace
undo log( 回 滚 日 志 )记 录 了 事 务 发 生 之 前 的 数 据 状 态( 不 包 括select)。
如果修改数据时出现异常,可以用 undo log 来实现回滚操作,保持原子性。
在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页上操作实现的,属于逻辑格式的日志。所谓逻辑日志是记录一个操作过程,不会物理删除undo log,sql执行更新操作都会记录一条undo日志。
redo Log(物理日志) 和 undo Log 与事务密切相关,统称为事务日志。
undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收缩,也可以单独创建一个 undo 表空间。
undo log有undo buffer,写undo log前,会先放在内存buffer中。
事务提交之前,就会先把undo log buffer里的数据刷新到undo log tablespace里。而且是在刷新redo log之前,就要先写undo log。
```java
-- 原值:name = yaxixi
update user set name = 'wakaka' where id = 1;
```
事务开始,从内存或磁盘取到这条数据,返回给 Server 的执行器
执行器修改这一行数据的值为 wakaka;
记录 name=yaxixi 到 undo log;
将undo log写入磁盘,进行持久化;
记录 name=wakaka 到 redo log;
调用存储引擎接口,在内存(Buffer Pool)中修改 name=wakaka;
事务提交。
内存和磁盘之间,工作着很多后台线程。
后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。
后台线程分为:
master thread;
负责刷新缓存数据到磁盘并协调调度其它后台进程。
IO thread;
分为 insert buffer、log、read、write 进程。分别用来处理 insert buffer、 重做日志、读写请求的 IO 回调。
purge thread;
用来回收 undo 页。
page cleaner thread
用来刷新脏页。
除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件,叫做binlog,它可以被所有的存储引擎使用。
binlog 以事件的形式记录了所有的 DDL 和 DML 语句(因为它记录的是操作而不是 数据值,属于逻辑日志),可以用来做主从复制和数据恢复。
跟 redo log 不一样,它的文件内容是可以追加的,没有固定大小限制。
在开启了 binlog 功能的情况下,可以把 binlog 导出成 SQL 语句,把所有的操作重放一遍,来实现数据的恢复。
binlog 的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器 的 binlog,然后执行一遍。
redo log和binglog的区别
redo log是InnoDB存储引擎特有的。binlog是MySQL的Server层实现的,所有引擎都可以使用。
redo log是物理日志,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来,不记录执行的语句信息,不是二进制文件。binlog是逻辑日志,是一个二进制格式的文件,用于记录用户对数据库更新的SQL语句信息。
redo log是循环写的,空间固定会用完。binlog是可以追加写入的。
追加写是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
XA是由X/Open组织提出的分布式事务的规范。
XA规范主要定义了(全局)事务管理器(TM: Transaction Manager)和(局部)资源管理器(RM: Resource Manager)之间的接口。
XA为了实现分布式事务,将事务的提交分成了两个阶段:也就是2PC (tow phase commit),XA协议就是通过将事务的提交分为两个阶段来实现分布式事务。
prepare 阶段:第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare准备提交请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成可以提交,然后把结果返回给事务管理器。
commit 阶段:事务管理器收到回应后进入第二阶段,如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把可以提交的事务回撤。
如果第一阶段中所有数据库都提交成功,那么事务管理器向数据库服务器发出确认提交请求,数据库服务器把事务的可以提交状态改为提交完成状态,然后返回应答。
MySQL中的XA实现分为:外部XA和内部XA。
外部XA是指我们通常意义上的分布式事务实现。
内部XA是指单台MySQL服务器中,Server层作为TM(事务协调者),而服务器中的多个数据库实例作为RM,而进行的一种分布式事务,也就是MySQL跨库事务;也就是一个事务涉及到同一条MySQL服务器中的两个innodb数据库(因为其它引擎不支持XA)。
为什么需求两阶段提交?
redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
在MySQL内部,在事务提交时利用两阶段提交(内部XA的两阶段提交)很好地解决了binlog和redo log的一致性问题。
事务的两阶段提交协议保证了无论在任何情况下,事务要么同时存在于存储引擎和binlog中,要么两个里面都不存在,这就保证了主库与从库之间数据的一致性。
如果数据库系统发生崩溃,当数据库系统重新启动时,会进行崩溃恢复操作。
存储引擎中处于prepare状态的事务,会去查询该事务是否也同时存在于binlog中。
如果存在,就在存储引擎内部提交该事务(因为此时从库可能已经获取了对应的binlog内容)。如果binlog中没有该事务,就回滚该事务。
当崩溃发生在写入redo log与写入binlog之间时,明显处于prepare状态的事务还没来得及写入到binlog中,所以该事务会在存储引擎内部进行回滚,这样该事务在存储引擎和binlog中都不会存在。
当崩溃发生在写完binlog与commit之间时,处于prepare状态的事务存在于binlog中,那么该事务会在存储引擎内部进行提交,这样该事务就同时存在于存储引擎和binlog中。
简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
redo log用于保证crash-safe能力。innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。
sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。这个参数也建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。
总结
在崩溃恢复时,判断事务是否需要提交,有如下几种情况:
binlog无记录,redo log无记录
在redo log 写之前恢复,恢复操作为回滚事务。
binlog无记录,redo log状态prepare
在binlog写完之前恢复,恢复操作为回滚事务。
binlog有记录,redo log状态prepare
在binlog写完之后,提交事务之前恢复,恢复操作为提交事务。
binlog有记录,redo log状态commit
正常完成的事务,不需要恢复。
所以,主要还是看binlog中有无记录。
update user set name = 'wakaka' where id = 1;
执行器先通过InnoDB的引擎接口查询id=1的这行数据,如果存在内存中,直接返回数据,否则先从磁盘读取到内存中,然后返回给执行器;
执行器拿到这行数据,修改name=‘wakaka’,获得新的一行数据,然后再调用InnoDB存储引擎的写接口将这行数据写入;
存储引擎将这行数据先存到内存中,同时将这次的更新操作记录到redo log中。此时redo log处于prepare的状态,没有commit。然后告知执行器操作完成,随时可以提交事务;
执行器生成这次更新操作的binlog,并把binlog写入磁盘;
执行器再调用存储引擎的提交事务接口,引擎把刚刚写入的redo log改成commit状态,更新操作完成。
第一阶段:InnoDB Prepare阶段。
此时SQL已经成功执行,并生成事务ID(xid)信息及redo和undo的内存日志。
此阶段InnoDB会写事务的redo log。
但要注意的是,redo log只是记录了事务的所有操作日志,并没有记录提交commit日志,因此事务此时的状态为prepare。此阶段对binlog不会有任何操作。
第二阶段:commit 阶段,这个阶段又分成两个步骤。
写binlog,先调用write()将binlog内存日志数据写入文件系统缓存,再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘。
完成事务的commit,此时在redo log中记录此事务的提交日志为commit状态。
流程图如下
吾御兮
MySQL架构与SQL执行流程详解