文章目录
1. 一条 SQL 语句执行步骤
我们的程序或者工具要操作数据库, 第一步要做什么事情?跟数据库建立连接
1.1 通信
首先,MySQL 必须要运行一个服务, 监听默认的 3306 端口.在我们开发系统跟第三方对接的时候, 必须要弄清楚的有两件事
- 第一个就是通信协议, 比如我们是用 HTTP 还是 WebService 还是 TCP?
- 第二个是消息格式, 比如我们用 XML 格式, 还是 JSON 格式, 还是定长格式?报文头长度多少, 包含什么内容, 每个字段的详细含义.比如我们之前跟银联对接, 银联的银行卡联网规范, 约定了一种比较复杂的通讯协议叫做: 四进四出单工异步长连接 (为了保证稳定性和性能)
MySQL 是支持多种通信协议的, 可以使用同步/异步的方式, 支持长连接/短连接, 这里我们拆分来看, 第一个是通信类型
1.1.1 通信类型
同步通信的特点:
- 同步通信依赖于被调用方, 受限于被调用方的性能.也就是说, 应用操作数据库, 线程会阻塞, 等待数据库的返回
- 一般只能做到一对一, 很难做到一对多的通信
异步跟同步相反:
- 异步可以避免应用阻塞等待, 但是不能节省 SQL 执行的时间
- 如果异步存在并发, 每一个 SQL 的执行都要单独建立一个连接, 避免数据混乱.但是这样会给服务端带来巨大的压力 (一个连接就会创建一个线程, 线程间切换会占用大量 CPU 资源).另外异步通信还带来了编码的复杂度, 所以一般不建议使用.如果要异步, 必须使用连接池, 排队从连接池获取连接而不是创建新连接
一般来说我们连接数据库都是同步连接
1.1.2 连接方式
MySQL 既支持短连接, 也支持长连接.短连接就是操作完毕以后, 马上 close 掉.长连接可以保持打开, 减少服务端创建和释放连接的消耗, 后面的程序访问的时候还可以使用这个连接.一般我们会在连接池中使用长连接
保持长连接会消耗内存.长时间不活动的连接,MySQL 服务器会断开
show global variables like 'wait_timeout'; -- 非交互式超时时间,如 JDBC 程序
show global variables like 'interactive_timeout'; -- 交互式超时时间,如数据库工具
默认都是 28800 秒,8 小时
我们怎么查看 MySQL 当前有多少个连接?
可以用 show status
命令:
show global status like 'Thread%';
Threads_cached: 缓存中的线程连接数
Threads_connected: 当前打开的连接数
Threads_created: 为处理连接创建的线程数
Threads_running: 非睡眠状态的连接数, 通常指并发连接数
每产生一个连接或者一个会话, 在服务端就会创建一个线程来处理.反过来, 如果要杀死会话, 就是 Kill 线程.有了连接数, 怎么知道当前连接的状态?也可以使用 SHOW PROCESS LIST;
(root 用户) 查看 SQL 的执行状态
https://dev.mysql.com/doc/refman/5.7/en/show-processlist.html
一些常见的状态:https://dev.mysql.com/doc/refman/5.7/en/thread-commands.html
状态 | 含义 |
---|---|
Sleep | 线程正在等待客户端, 以向它发送一个新语句 |
Query | 线程正在执行查询或往客户端发送数据 |
Copyingtotmptableondisk | 临时结果集合大于 tmp_table_size.线程把临时表从存储器内部格式改变为磁盘模式, 以节约存储器 |
Sendingdata | 线程正在为 SELECT 语句处理行, 同时正在向客户端发送数据 |
Sortingforgroup | 线程正在进行分类, 以满足 GROUPBY 要求 |
Sortingfororder | 线程正在进行分类, 以满足 ORDERBY 要求 |
MySQL 服务允许的最大连接数是多少呢?
在 5.7 版本中默认是 151 个, 最大可以设置成 16384(2^14)
show variables like 'max_connections';
show 的参数说明:
- 级别: 会话 session 级别 (默认);全局 global 级别
- 动态修改:set, 重启后失效;永久生效, 修改配置文件/etc/my.cnf
set global max_connections = 1000;
1.1.3 通信协议
MySQL 支持哪些通信协议呢?
第一种是 UnixSocket.比如我们在 Linux 服务器上, 如果没有指定-h 参数, 它就用 socket 方式登录 (省略了-S/var/lib/mysql/mysql.sock)
它不用通过网络协议, 也可以连接到 MySQL 的服务器, 它需要用到服务器上的一个物理文件 (/var/lib/mysql/mysql.sock)
select @@socket;
如果指定-h 参数, 就会用第二种方式,TCP/IP 协议
mysql -h 192.168.8.211 -u root -p 123456
我们的编程语言的连接模块都是用 TCP 协议连接到 MySQL 服务器的, 比如 mysql-connector-java-x.x.xx.jar
另外还有命名管道 (Named Pipes) 和内存共享 (Share Memory) 的方式, 这两种通信方式只能在 Windows 上面使用, 一般用得比较少
1.1.4 通信方式
第二个是通信方式
- 单工: 在两台计算机通信的时候, 数据的传输是单向的.生活中的类比: 遥控器
- 半双工: 在两台计算机之间, 数据传输是双向的, 你可以给我发送, 我也可以给你发送, 但是在这个通讯连接里面, 同一时间只能有一台服务器在发送数据, 也就是你要给我发的话, 也必须等我发给你完了之后才能给我发.生活中的类比: 对讲机
- 全双工: 数据的传输是双向的, 并且可以同时传输.生活中的类比: 打电话
MySQL 使用了半双工的通信方式?
要么是客户端向服务端发送数据, 要么是服务端向客户端发送数据, 这两个动作不能同时发生.所以客户端发送 SQL 语句给服务端的时候,(在一次连接里面) 数据是不能分成小块发送的, 不管你的 SQL 语句有多大, 都是一次性发送
比如我们用 MyBatis 动态 SQL 生成了一个批量插入的语句, 插入 10 万条数据,values 后面跟了一长串的内容, 或者 where 条件 in 里面的值太多, 会出现问题
这个时候我们必须要调整 MySQL 服务器配置 max_allowed_packet 参数的值 (默认是 4M), 把它调大, 否则就会报错
另一方面, 对于服务端来说, 也是一次性发送所有的数据, 不能因为你已经取到了想要的数据就中断操作, 这个时候会对网络和内存产生大量消耗
所以, 我们一定要在程序里面避免不带 limit 的这种操作, 比如一次把所有满足条件的数据全部查出来, 一定要先 count 一下.如果数据量的话, 可以分批查询
执行一条查询语句, 客户端跟服务端建立连接之后呢?下一步要做什么?
1.2 查询缓存
MySQL 内部自带了一个缓存模块
缓存的作用我们应该很清楚了, 把数据以 KV 的形式放到内存里面, 可以加快数据的读取速度, 也可以减少服务器处理的时间.但是 MySQL 的缓存我们好像比较陌生, 从来没有去配置过, 也不知道它什么时候生效?
比如 user_innodb 有 500 万行数据, 没有索引.我们在没有索引的字段上执行同样的查询, 大家觉得第二次会快吗?
select * from user_innodb where name='青山';
缓存没有生效, 为什么?MySQL 的缓存默认是关闭的
show variables like 'query_cache%';
默认关闭的意思就是不推荐使用, 为什么 MySQL 不推荐使用它自带的缓存呢?
主要是因为 MySQL 自带的缓存的应用场景有限, 第一个是它要求 SQL 语句必须一模一样, 中间多一个空格, 字母大小写不同都被认为是不同的的 SQL
第二个是表里面任何一条数据发生变化的时候, 这张表所有缓存都会失效, 所以对于有大量数据更新的应用, 也不适合
所以缓存这一块, 我们还是交给 ORM 框架 (比如 MyBatis 默认开启了一级缓存), 或者独立的缓存服务, 比如 Redis 来处理更合适.在 MySQL8.0 中, 查询缓存已经被移除了
1.3 语法解析和预处理 (Parser & Preprocessor)
我们没有使用缓存的话, 就会跳过缓存的模块, 下一步我们要做什么呢?
OK, 这里我会有一个疑问, 为什么我的一条 SQL 语句能够被识别呢?假如我随便执行一个字符串 penyuyan, 服务器报了一个 1064 的错:
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'penyuyan' at line 1
它是怎么知道我输入的内容是错误的?
这个就是 MySQL 的 Parser 解析器和 Preprocessor 预处理模块.这一步主要做的事情是对语句基于 SQL 语法进行词法和语法分析和语义的解析
1.3.1 词法解析
词法分析就是把一个完整的 SQL 语句打碎成一个个的单词
比如一个简单的 SQL 语句:
select name from user where id = 1;
它会打碎成 8 个符号, 每个符号是什么类型, 从哪里开始到哪里结束
1.3.2 语法解析
第二步就是语法分析, 语法分析会对 SQL 做一些语法检查, 比如单引号有没有闭合, 然后根据 MySQL 定义的语法规则, 根据 SQL 语句生成一个数据结构.这个数据结构我们把它叫做解析树 (select_lex)
任何数据库的中间件, 比如 Mycat,Sharding-JDBC(用到了 DruidParser), 都必须要有词法和语法分析功能, 在市面上也有很多的开源的词法解析的工具 (比如 LEX,Yacc)
1.4 预处理器
问题: 如果我写了一个词法和语法都正确的 SQL, 但是表名或者字段不存在, 会在哪里报错?是在数据库的执行层还是解析器?比如:
select * from penyuyan;
解析器可以分析语法, 但是它怎么知道数据库里面有什么表, 表里面有什么字段呢?
实际上还是在解析的时候报错, 解析 SQL 的环节里面有个预处理器.
它会检查生成的解析树, 解决解析器无法解析的语义.比如, 它会检查表和列名是否存在, 检查名字和别名, 保证没有歧义.
预处理之后得到一个新的解析树
1.5 查询优化 (Query Optimizer) 与查询执行计划
1.5.1 什么是优化器?
得到解析树之后, 是不是执行 SQL 语句了呢
这里我们有一个问题, 一条 SQL 语句是不是只有一种执行方式?或者说数据库最终执行的 SQL 是不是就是我们发送的 SQL?
这个答案是否定的.一条 SQL 语句是可以有很多种执行方式的, 最终返回相同的结果, 他们是等价的.但是如果有这么多种执行方式, 这些执行方式怎么得到的?最终选择哪一种去执行?根据什么判断标准去选择?
这个就是 MySQL 的查询优化器的模块 (Optimizer).
查询优化器的目的就是根据解析树生成不同的执行计划 (ExecutionPlan), 然后选择一种最优的执行计划,MySQL 里面使用的是基于开销 (cost) 的优化器, 那种执行计划开销最小, 就用哪种.
可以使用这个命令查看查询的开销:
show status like 'Last_query_cost';
https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Last_query_cost
1.5.2 优化器可以做什么?
MySQL 的优化器能处理哪些优化类型呢?
举两个简单的例子:
- 当我们对多张表进行关联查询的时候, 以哪个表的数据作为基准表
- 有多个索引可以使用的时候, 选择哪个索引
实际上, 对于每一种数据库来说, 优化器的模块都是必不可少的, 他们通过复杂的算法实现尽可能优化查询效率的目标
如果对于优化器的细节感兴趣, 可以看看《数据库查询优化器的艺术-原理解析与 SQL 性能优化》
但是优化器也不是万能的, 并不是再垃圾的 SQL 语句都能自动优化, 也不是每次都能选择到最优的执行计划, 大家在编写 SQL 语句的时候还是要注意
如果我们想知道优化器是怎么工作的, 它生成了几种执行计划, 每种执行计划的 cost 是多少, 应该怎么做
1.5.3 优化器是怎么得到执行计划的?
https://dev.mysql.com/doc/internals/en/optimizer-tracing.html
首先我们要启用优化器的追踪 (默认是关闭的):
SHOW VARIABLES LIKE 'optimizer_trace';
set optimizer_trace='enabled=on';
注意开启这开关是会消耗性能的, 因为它要把优化分析的结果写到表里面, 所以不要轻易开启, 或者查看完之后关闭它 (改成 off)
注意: 参数分为 session 和 global 级别
接着我们执行一个 SQL 语句, 优化器会生成执行计划:
select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid;
这个时候优化器分析的过程已经记录到系统表里面了, 我们可以查询:
select * from information_schema.optimizer_trace\G
它是一个 JSON 类型的数据, 主要分成三部分, 准备阶段, 优化阶段和执行阶段
expanded_query 是优化后的 SQL 语句.considered_execution_plans 里面列出了所有的执行计划.分析完记得关掉它:
set optimizer_trace="enabled=off"; SHOW VARIABLES LIKE 'optimizer_trace';
1.5.4 优化器得到的结果
优化完之后, 得到一个什么东西呢?
优化器最终会把解析树变成一个查询执行计划, 查询执行计划是一个数据结构
当然, 这个执行计划是不是一定是最优的执行计划呢?不一定, 因为 MySQL 也有可能覆盖不到所有的执行计划
我们怎么查看 MySQL 的执行计划呢?比如多张表关联查询, 先查询哪张表?在执行查询的时候可能用到哪些索引, 实际上用到了什么索引?
MySQL 提供了一个执行计划的工具.我们在 SQL 语句前面加上 EXPLAIN, 就可以看到执行计划的信息
EXPLAIN select name from user where id=1;
注意 Explain 的结果也不一定最终执行的方式
1.6 存储引擎
1.6.1 MySQL 8.0 Supported Storage Engines
1.6.1.1 InnoDB
MySQL 8.0 中的默认存储引擎. InnoDB 是一个事务安全 (ACID 兼容) 的 MySQL 存储引擎, 具有提交, 回滚和崩溃恢复功能来保护用户数据. InnoDB 行级锁 (不需要升级到更粗粒度的锁) 和 oracle 风格的一致非锁定读取提高了多用户并发性和性能. InnoDB 将用户数据存储在聚集索引中, 以减少基于主键的常见查询的 I/O. 为了维护数据的完整性, InnoDB 还支持 FOREIGN KEY 引用完整性约束
1.6.1.2 MyISAM
这些表占用空间小. 表级锁定限制了读/写工作负载中的性能, 因此它经常用于 Web 和数据仓库配置中的只读或以读为主的工作负载中
1.6.1.3 Memory
将所有数据存储在 RAM 中, 以便在需要快速查找非关键数据的环境中进行快速访问. 这个引擎以前被称为 HEAP 引擎. 它的用例正在减少;InnoDB 和它的缓冲池内存区域提供了一种通用且持久的方式来保存大部分或所有的数据在内存中, 而 NDBCLUSTER 为巨大的分布式数据集提供了快速的键值查找
1.6.1.4 CSV
它的表实际上是具有逗号分隔值的文本文件. CSV 表允许您以 CSV 格式导入或转储数据, 以便与读写相同格式的脚本和应用程序交换数据. 由于 CSV 表没有建立索引, 通常在正常操作期间, 您会将数据保存在 InnoDB 表中, 并且只在导入或导出阶段使用 CSV 表
1.6.1.5 Archive
这些紧凑的, 未索引的表用于存储和检索大量很少引用的历史, 存档或安全审计信息
1.6.1.6 Blackhole
Blackhole 存储引擎接受但不存储数据, 类似于 Unix /dev/null 设备. 查询总是返回一个空集. 这些表可以在复制配置中使用, 其中 DML 语句被发送到复制服务器, 但源服务器不保留自己的数据副本
1.6.1.7 NDB(也称为 NDBCLUSTER)
这个集群数据库引擎特别适合于需要最高正常运行时间和可用性的应用程序
1.6.1.8 Merge
允许 MySQL DBA 或开发人员将一系列相同的 MyISAM 表进行逻辑分组, 并将它们作为一个对象引用. 适用于数据仓库等 VLDB 环境
1.6.1.9 Federated
提供连接独立的 MySQL 服务器的能力, 从多个物理服务器创建一个逻辑数据库. 非常适合分布式或数据集市环境
1.6.1.10 Example
这个引擎作为 MySQL 源代码中的一个示例, 说明了如何开始编写新的存储引擎. 它主要是开发人员感兴趣的. 存储引擎是一个什么都不做的“存根”. 您可以使用该引擎创建表, 但不能在表中存储数据或从表中检索数据
1.6.2 存储引擎特性总结
特性 | MyISAM | Memory | InnoDB | Archive | NDB |
---|---|---|---|---|---|
B-tree indexes | Yes | Yes | Yes | No | No |
Backup/point-in-time recovery (note 1) | Yes | Yes | Yes | Yes | Yes |
Cluster database support | No | No | No | No | Yes |
Clustered indexes | No | No | Yes | No | No |
Compressed data | Yes (note 2) | No | Yes | Yes | No |
Data caches | No | N/A | Yes | No | Yes |
Encrypted data | Yes (note 3) | Yes (note 3) | Yes (note 4) | Yes (note 3) | Yes (note 3) |
Foreign key support | No | No | Yes | No | Yes (note 5) |
Full-text search indexes | Yes | No | Yes (note 6) | No | No |
Geospatial data type support | Yes | No | Yes | Yes | Yes |
Geospatial indexing support | Yes | No | Yes (note 7) | No | No |
Hash indexes | No | Yes | No (note 8) | No | Yes |
Index caches | Yes | N/A | Yes | No | Yes |
Locking granularity | Table | Table | Row | Row | Row |
MVCC | No | No | Yes | No | No |
Replication support (note 1) | Yes | Limited | Yes | Yes | Yes |
Storage limits | 256TB | RAM | 64TB | None | 384EB |
T-tree indexes | No | No | No | No | Yes |
Transactions | No | No | Yes | No | Yes |
Update statistics for data dictionary | Yes | Yes | Yes | Yes | Yes |
Notes
-
- 在服务器中实现, 而不是在存储引擎中实现
-
- 只有在使用压缩行格式时, 才支持压缩的 MyISAM 表, 使用 MyISAM 压缩行格式的表是只读的
-
- 通过加密功能在服务器中实现
-
- 在服务器端实现加密功能, 在 MySQL 5.7 及以后版本中, 支持静态数据加密
-
- Support for foreign keys 在 MySQL 集群 NDB 7.3 及更高版本中是可用的
-
- Support for FULLTEXT indexes 在 MySQL 5.6 和更高版本中是可用的
-
- Support for geospatial indexing 在 MySQL 5.7 和更高版本中是可用的
-
- InnoDB 内部利用哈希索引来实现自适应哈希索引特性
1.7 执行引擎 (Query Execution Engine)
存储引擎分析完了, 它是我们存储数据的形式, 执行引擎使用执行计划去操作存储引擎, 它利用存储引擎提供的相应的 API 来完成操作
不同功能的存储引擎需要实现相同的 API, 修改表的存储引擎类型, 执行引擎不用做任何调用修改
2. MySQL 体系结构
2.1 模块详解
- Connectors: 用来支持各种语言和 SQL 的交互, 比如 PHP,Python,Java 的 JDBC
- Management Serveices & Utilities: 系统管理和控制工具, 包括备份恢复,MySQL 复制, 集群等等
- Connection Pool: 连接池, 管理需要缓冲的资源, 包括用户密码权限线程等
- SQL Interface: 用来接收用户的 SQL 命令, 返回用户需要的查询结果
- Parser: 用来解析 SQL 语句
- Optimizer: 查询优化器
- Cache and Buffer: 查询缓存, 除了行记录的缓存之外, 还有表缓存,Key 缓存, 权限缓存等等
- Pluggable Storage Engines: 插件式存储引擎, 它提供 API 给服务层使用, 跟具体的文件打交道
2.2 架构分层
总体上, 我们可以把 MySQL 分成三层, 跟客户端对接的连接层, 真正执行操作的服务层, 和跟硬件打交道的存储引擎层 (参考 MyBatis: 接口, 核心, 基础)
2.2.1 连接层
我们的客户端要连接到 MySQL 服务器 3306 端口, 必须要跟服务端建立连接, 那么管理所有的连接, 验证客户端的身份和权限, 这些功能就在连接层完成
2.2.2 服务层
连接层会把 SQL 语句交给服务层, 这里面又包含一系列的流程:
比如查询缓存的判断, 根据 SQL 调用相应的接口, 对我们的 SQL 语句进行词法和语法的解析 (比如关键字怎么识别, 别名怎么识别, 语法有没有错误等等)
然后就是优化器,MySQL 底层会根据一定的规则对我们的 SQL 语句进行优化, 最后再交给执行器去执行
2.2.3 存储引擎
存储引擎就是我们的数据真正存放的地方, 在 MySQL 里面支持不同的存储引擎
再往下就是内存或者磁盘
3. InnoDB Memory Structures And Disk Structures
3.1 InnoDB Memory Structures
https://dev.mysql.com/doc/refman/8.0/en/innodb-in-memory-structures.html
InnoDB Memory Structures 分为 4 个部分
- Buffer Pool
- Change Buffer
- Adaptive Hash Index
- Log Buffer
3.1.1 Buffer Pool
buffer pool 是 InnoDB 用于缓存表和索引数据, buffer pool 允许从内存中直接访问经常使用的数据加快处理速度, 在专用服务器上, 多达 80% 的物理内存通常分配给 buffer pool
为了提高大容量读操作的效率, buffer pool 被划分为可包含多行的页 (page), 为了提高缓存管理的效率, buffer pool 被实现为 page 的链表, 很少使用的数据将使用 LRU 算法从缓存中淘汰
3.1.1.1 Buffer Pool LRU Algorithm
buffer pool 使用 LRU 算法作为列表来管理, 当需要空间将新的 page 添加到 buffer pool 时, 最近使用最少的 page 将被淘汰, 并将新的 page 添加到列表的中间点 (midpoint), 这种中间点插入策略将列表分为两个子列表
- 在顶部是一个最近被访问过的 new 的子列表
- 在尾部是最近被访问次数较少的 old 的子列表
buffer pool 的 LRU 算法详细说明
- 3/8 的缓冲池用于 old 的子列表
- 列表的中间点是 new 子列表的尾部与 old 子列表的头部相遇的边界
- 当 InnoDB 读取一个 page 到缓冲池时, 它首先将其插入到中间点 (old 子列表的 head), 发生场景比如 SQL 查询, 或者是 InnoDB 自动执行的预读操作的一部分
- 访问 old 子列表中的 page 会使其年轻, 将其移到 new 子列表的头部, 如果读取 page 是因为用户发起的操作将会立即执行, 如果是由于预读操作而读取的, 那么第一次访问不会立即发生, 在淘汰 page 之前可能根本不会发生
- 当数据库运行时, buffer pool 中未被访问的 page 会移到列表的尾部, 由于在中间点插入 page, old 子列表中的页面也会老化. 最终未使用的 page 到达 old 子列表的尾部并被淘汰
Buffer Pool 默认大小是 128M(134217728 字节), 可以调整 (BUFFER_POOL_SIZE 性能调优)
这些参数都可以在官网查到详细的含义, 用搜索功能
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rO4aCuaA-1644908827644)(https://huaweirookie.oss-cn-shenzhen.aliyuncs.com/20210226162707.png)]
3.1.2 Change Buffer
change buffer 是一种特殊的数据结构, 当辅助索引页不在缓冲池中时, 它会将更改缓存在辅助索引页中. 缓冲后的更改 (可能来自 INSERT, UPDATE 或 DELETE 操作 (DML)) 在稍后由其他读操作将页面加载到缓冲池时合并
与聚集索引不同, 辅助索引通常是非唯一的, 插入辅助索引的顺序相对随机. 类似地, 删除和更新可能会影响索引树中不相邻的辅助索引页. 稍后, 当其他操作将受影响的页面读入缓冲池时, 合并缓存的更改, 可以避免大量随机访问 I/O, 这些随机访问 I/O 将需要从磁盘将辅助索引页读入缓冲池
清除操作在系统大部分空闲或缓慢关闭期间运行, 定期将更新的索引页写入磁盘. 清除操作可以为一系列索引值写磁盘块, 比立即将每个值写到磁盘更有效
如果数据库大部分索引都是非唯一索引, 并且业务是写多读少, 不会在写数据后立刻读取, 就可以使用 ChangeBuffer(写缓冲).写多读少的业务, 调大这个值:
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
代表 Change Buffer 占 Buffer Pool 的比例, 默认 25%
3.1.3 Adaptive Hash Index
Adaptive Hash Index 使 InnoDB 在具有适当的工作负载组合和足够的缓冲池内存的系统上更像一个内存中的数据库, 而不会牺牲事务特性或可靠性. 自适应哈希索引由 innodb_adaptive_hash_index 变量启用, 或者在服务器启动时通过 skip-innodb- adaptivehash -index
关闭
根据观察到的搜索模式, 使用索引键的前缀构建哈希索引. 前缀可以是任意长度的, 并且可能只有 b 树中的一些值出现在哈希索引中. 哈希索引是根据索引中经常被访问的页面的需求构建的
如果一个表几乎完全可以放在主内存中, 那么哈希索引通过允许直接查找任何元素, 将索引值转换为某种指针, 从而加快查询速度. InnoDB 有一个监控索引搜索的机制. 如果 InnoDB 注意到查询可以从构建哈希索引中获益, 它会自动这么做
对于某些工作负载, 哈希索引查找的加速大大超过了监视索引查找和维护哈希索引结构所需的额外工作. 对自适应哈希索引的访问有时会成为繁重工作负载下的争用源, 例如多个并发连接. 使用 LIKE 操作符和 % 通配符的查询也不会受益. 对于不能从自适应哈希索引中受益的工作负载, 关闭它可以减少不必要的性能开销. 由于难以预先预测自适应哈希索引是否适合于某一特定的系统
对自适应哈希索引特性进行了分区. 每个索引被绑定到一个特定的分区, 每个分区由一个单独的锁存器保护. 分区由 innodb_adaptive_hash_index_parts 变量控制. innodb_adaptive_hash_index_parts 变量默认设置为 8. 最大可设置为 512
你可以在 SHOW ENGINE INNODB STATUS
输出的 SEMAPHORES
部分监控自适应哈希索引的使用和争用. 如果有许多线程在等待 btr0sea.c 中创建的 rw-latch, 考虑增加自适应哈希索引分区的数量或禁用自适应哈希索引
3.1.4 Log Buffer
日志缓冲区是存放要写入磁盘上日志文件的数据的内存区域. 日志缓冲区大小由 innodb_log_buffer_size 变量定义, 默认大小为 16MB, 日志缓冲区的内容会定期刷新到磁盘, 大的日志缓冲区使大的事务能够运行, 而不需要在事务提交之前将 redo log 数据写入磁盘, 因此, 如果事务更新, 插入或删除许多行, 增加日志缓冲区的大小可以节省磁盘 I/O
innodb_flush_log_at_trx_commit
变量控制如何将日志缓冲区的内容写入和刷新到磁盘
innodb_flush_log_at_timeout
变量控制日志刷新频率
思考一个问题: 如果 Buffer Pool 里面的脏页还没有刷入磁盘时, 数据库宕机或者重启, 这些数据丢失.如果写操作写到一半, 甚至可能会破坏数据文件导致数据库不可用
为了避免这个问题,InnoDB 把所有对页面的修改操作专门写入一个日志文件, 并且在数据库启动时从这个文件进行恢复操作 (实现 crash-safe)——用它来实现事务的持久性
这个文件就是磁盘的 redolog(叫做重做日志), 对应于 /var/lib/mysql/
目录下的 ib_logfile0 和 ib_logfile1, 每个 48M 这种日志和磁盘配合的整个过程, 其实就是 MySQL 里的 WAL 技术 (Write-Ahead Logging), 它的关键点就是先写日志, 再写磁盘
show variables like 'innodb_log%';
值 | 含义 |
---|---|
innodb_log_file_size | 指定每个文件的大小, 默认 48M |
innodb_log_files_in_group | 指定文件的数量, 默认为 2 |
innodb_log_group_home_dir | 指定文件所在路径, 相对或绝对, 如果不指定, 则为 datadir 路径 |
问题:
同样是写磁盘, 为什么不直接写到 dbfile 里面去?为什么先写日志再写磁盘?
我们先来了解一下随机 I/O 和顺序 I/O 的概念
磁盘的最小组成单元是扇区, 通常是 512 个字节
操作系统和内存打交道, 最小的单位是页 Page
操作系统和磁盘打交道, 读写磁盘, 最小的单位是块 Block
如果我们所需要的数据是随机分散在不同页的不同扇区中, 那么找到相应的数据需要等到磁臂旋转到指定的页, 然后盘片寻找到对应的扇区, 才能找到我们所需要的一块数据, 一次进行此过程直到找完所有数据, 这个就是随机 IO, 读取数据速度较慢
假设我们已经找到了第一块数据, 并且其他所需的数据就在这一块数据后边, 那么就不需要重新寻址, 可以依次拿到我们所需的数据, 这个就叫顺序 IO
刷盘是随机 I/O, 而记录日志是顺序 I/O, 顺序 I/O 效率更高.因此先把修改写入日志, 可以延迟刷盘时机, 进而提升系统吞吐
当然 redolog 也不是每一次都直接写入磁盘, 在 Buffer Pool 里面有一块内存区域 (Log Buffer) 专门用来保存即将要写入日志文件的数据, 默认 16M, 它一样可以节省\磁盘 IO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HsZmvwXO-1644908827647)(https://huaweirookie.oss-cn-shenzhen.aliyuncs.com/xxxx.png)]
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
需要注意:redolog 的内容主要是用于崩溃恢复.磁盘的数据文件, 数据来自 buffer pool.redolog 写入磁盘, 不是写入数据文件
那么,LogBuffer 什么时候写入 logfile?
在我们写入数据到磁盘的时候, 操作系统本身是有缓存的.flush 就是把操作系统缓冲区写入到磁盘
logbuffer 写入磁盘的时机, 由一个参数控制, 默认是 1
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_flush_log_at_trx_commit
值 | 含义 |
---|---|
0(延迟写) | logbuffer 将每秒一次地写入 logfile 中, 并且 logfile 的 flush 操作同时进行.该模式下, 在事务提交的时候, 不会主动触发写入磁盘的操作 |
1(默认, 实时写, 实时刷) | 每次事务提交时 MySQL 都会把 logbuffer 的数据写入 logfile, 并且刷到磁盘中去 |
2(实时写, 延迟刷) | 每次事务提交时 MySQL 都会把 logbuffer 的数据写入 logfile.但是 flush 操作并不会同时进行.该模式下 MySQL 会每秒执行一次 flush 操作 |
这是内存结构的第 4 块内容,redolog, 它又分成内存和磁盘两部分.redolog 有什么特点?
- redolog 是 InnoDB 存储引擎实现的, 并不是所有存储引擎都有
- 不是记录数据页更新之后的状态, 而是记录这个页做了什么改动, 属于物理日志
- redolog 的大小是固定的, 前面的内容会被覆盖
checkpoint 是当前要覆盖的位置.如果 writep os 跟 check point 重叠, 说明 redolog 已经写满, 这时候需要同步 redolog 到磁盘中
这是 MySQL 的内存结构, 总结一下, 分为:Buffer pool,change buffer,Adaptive Hash Index,logbuffer
磁盘结构里面主要是各种各样的表空间, 叫做 Tablespace
3.2 InnoDB On-Disk Structures
3.2.1 系统表空间 (System Tablespaces)
在默认情况下 InnoDB 存储引擎有一个共享表空间 (对应文件 /var/lib/mysql/ibdata1
), 也叫系统表空间
InnoDB 系统表空间包含 InnoDB Data Dictionary, Doublewrite Buffer, Change Buffer, Undo Logs, 如果没有指定 file-per-table, 也包含用户创建的表和索引数据
- undo 在后面介绍, 因为有独立的表空间
- 数据字典: 由内部系统表组成, 存储表和索引的元数据 (定义信息)
- 双写缓冲 (InnoDB 的一大特性):InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为 16K, 操作系统页大小为 4K,InnoDB 的页写入到磁盘时, 一个页需要分 4 次写
如果存储引擎正在写入页的数据到磁盘时发生了宕机, 可能出现页只写了一部分的情况, 比如只写了 4K, 就宕机了, 这种情况叫做部分写失效 (partial page write), 可能会导致数据丢失
show variables like 'innodb_doublewrite';
我们不是有 redo log 吗?但是有个问题, 如果这个页本身已经损坏了, 用它来做崩溃恢复是没有意义的.所以在对于应用 redo log 之前, 需要一个页的副本.如果出现了写入失效, 就用页的副本来还原这个页, 然后再应用 redolog.这个页的副本就是 double write,InnoDB 的双写技术.通过它实现了数据页的可靠性.跟 redo log 一样,double write 由两部分组成, 一部分是内存的 double write, 一个部分是磁盘上的 double write.因为 double write 是顺序写入的, 不会带来很大的开销
在默认情况下, 所有的表共享一个系统表空间, 这个文件会越来越大, 而且它的空间不会收缩
系统表空间数据文件的大小和数量由 innodb_data_file_path
启动选项定义
3.2.2 独占表空间 (File-Per-Table Tablespaces)
我们可以让每张表独占一个表空间.这个开关通过 innodb_file_per_table 设置, 默认开启
SHOW VARIABLES LIKE 'innodb_file_per_table';
开启后, 则每张表会开辟一个表空间, 这个文件就是数据目录下的 ibd 文件 (例如 /var/lib/mysql/gupao/user_innodb.ibd
), 存放表的索引和数据
但是其他类的数据, 如回滚 (undo) 信息, 插入缓冲索引页, 系统事务信息, 二次写缓冲 (Double write buffer) 等还是存放在原来的共享表空间内
3.2.3 通用表空间 (General Tablespaces)
通用表空间也是一种共享的表空间, 跟 ibdata1 类似
可以创建一个通用的表空间, 用来存储不同数据库的表, 数据路径和文件可以自定义.语法:
create tablespace ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb;
在创建表的时候可以指定表空间, 用 ALTER 修改表空间可以转移表空间
create table t2673(id integer)tablespace ts2673;
不同表空间的数据是可以移动的
删除表空间需要先删除里面的所有表:
drop table t2673;
drop tablespace ts2673;
3.2.4 临时表空间 (Temporary Tablespaces)
存储临时表的数据, 包括用户创建的临时表, 和磁盘的内部临时表.对应数据目录下的 ibtmp1 文件.当数据服务器正常关闭时, 该表空间被删除, 下次重新产生
3.2.5 Undo Log Tablespaces
https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-tablespaces.html
https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.html
undolog(撤销日志或回滚日志) 记录了事务发生之前的数据状态 (不包括 select).如果修改数据时出现异常, 可以用 undolog 来实现回滚操作 (保持原子性)
在执行 undo 的时候, 仅仅是将数据从逻辑上恢复至事务之前的状态, 而不是从物理页面上操作实现的, 属于逻辑格式的日志
redoLog 和 undoLog 与事务密切相关, 统称为事务日志
undoLog 的数据默认在系统表空间 ibdata1 文件中, 因为共享表空间不会自动收缩, 也可以单独创建一个 undo 表空间
show global variables like '%undo%';
有了这些日志之后, 我们来总结一下一个更新操作的流程, 这是一个简化的过程.name 原值是 qingshan
update user set name = 'penyuyan' where id=1;
- 事务开始, 从内存或磁盘取到这条数据, 返回给 Server 的执行器
- 执行器修改这一行数据的值为 penyuyan;
- 记录 name=qingshan 到 undolog;
- 记录 name=penyuyan 到 redolog;
- 调用存储引擎接口, 在内存 (Buffer Pool) 中修改 name=penyuyan;
- 事务提交
内存和磁盘之间, 工作着很多后台线程
3.2.6 Redo log
磁盘结构里面的 redo log, 在前面已经介绍过了
3.3 后台线程
后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘.后台线程分为
- master thread 负责刷新缓存数据到磁盘并协调调度其它后台进程
- IO thread 分为 insert buffer,log,read,write 进程.分别用来处理 insert buffer, 重做日志, 读写请求的 IO 回调
- purge thread 用来回收 undo 页
- page cleaner thread 用来刷新脏页
除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件, 叫做 binlog, 它可以被所有的存储引擎使用
4. Binlog
https://dev.mysql.com/doc/refman/5.7/en/binary-log.html
binlog 以事件的形式记录了所有的 DDL 和 DML 语句 (因为它记录的是操作而不是数据值, 属于逻辑日志), 可以用来做主从复制和数据恢复
跟 redo log 不一样, 它的文件内容是可以追加的, 没有固定大小限制
在开启了 binlog 功能的情况下, 我们可以把 binlog 导出成 SQL 语句, 把所有的操作重放一遍, 来实现数据的恢复
binlog 的另一个功能就是用来实现主从复制, 它的原理就是从服务器读取主服务器的 binlog, 然后执行一遍
有了这两个日志之后, 我们来看一下一条更新语句是怎么执行的:
例如一条语句:update teacher set name='彭于晏' where id = 1
- 先查询到这条数据, 如果有缓存, 也会用到缓存
- 把 name 改成盆鱼宴, 然后调用引擎的 API 接口, 写入这一行数据到内存, 同时记录 redo log.这时 redo log 进入 prepare 状态, 然后告诉执行器, 执行完成了, 可以随时提交
- 执行器收到通知后记录 binlog, 然后调用存储引擎接口, 设置 redo log 为 commit 状态
- 更新完成
这张图片的重点:
- 先记录到内存, 再写日志文件
- 记录 redolog 分为两个阶段
- 存储引擎和 Server 记录不同的日志
- 先记录 redo, 再记录 binlog
4.1 DML 与 DDL
DML(Data Manipulation Language) 数据操纵语言: 对数据库中的数据进行一些简单操作, 如 insert,delete,update,select 等
DDL(Data Definition Language) 数据定义语言: 对数据库中的某些对象 (例如,database,table) 进行管理, 如 Create,Alter 和 Drop.truncate
- DML 操作是需要手动控制事务的开启, 提交 (commit) 和回滚的
- DDL 操作是隐性提交的, 不能 rollback