一,一条SQL的查询流程

  1. 去连接池获取连接
  2. 查询缓存,命中返回,否则继续向下
  3. 词法解析&预处理

    词法解析拆分SQL,语法分析检查SQL的正确性生成一颗解析树,预处理检查表名,列名,生成一颗解析树。

  4. 优化器优化,优化计划,查询计划

  5. 执行引擎生成执行计划
  6. 存储引擎查询SQL,加入缓存,返回结果。

image.png

1.获取连接

MySQL支持多种通信协议,可以使用同步/异步的方式,支持长连接,短连接。

1.1 通信类型

一般来说,连接数据库都是同步连接

  1. 同步连接:依赖于被调用方,受限制于被调用方的性能;一般只能一对一。
  2. 异步连接:避免阻塞,但不能节省SQL的执行时间,并发情况下,每个SQL的执行都要单独建立连接,占用大量CPU资源;异步连接必须使用连接池减少线程创建销毁的开销。

1.2 连接方式

MySQL长短连接都支持,一般我们会在连接池中使用长连接。保持长连接会消耗内存,长时间不活动的连接,MySQL服务器会断开。

  1. show global variables like 'wait_timeout'; -- 非交互式超时时间,如 JDBC 程序
  2. show global variables like 'interactive_timeout'; -- 交互式超时时间,如数据库工具

默认长连接断开时间是8小时。

可以使用 show status;查看当前MySQL有多少个连接。

  1. show global status like 'Thread%';
Threads_cached 缓存中的线程连接数
Threads_connected 当前打开的连接数
Threads_created 为处理连接创建的线程数
Threads_running 非睡眠状态的连接数,通常指并发连接数

每产生一个连接或者会话,服务端就会创建一个线程来处理。杀死会话本质就是kill 线程。

可以使用SHOW PROCESSLIST;(root 用户)查看 SQL 的执行状态。

+——+———+—————-+———+————-+———+—————+ | Id | User | Host | db | Command | Time | State | Info | +——+———+—————-+———+————-+———+—————+ | 11 | root | localhost | NULL | Query | 0 | starting | show processlist | +——+———+—————-+———+————-+———+—————+

状态 含义
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 要求

在5.7版本,MySQL的默认连接数是151个,我们最大可以修改为16384个 (214)。

  1. show variables like 'max_connections';
  2. set [global | session] max_connections =10000;

1.3 通信协议

  1. 编程语言的连接模块都是用 TCP 协议连接到 MySQL 服务器的,比如mysql-connector-java-x.x.xx.jar。
  2. 类unix系统上,支持 Socket套接字文件进行进程间通信。/tmp/mysql.sock
  3. windows系统上还支持命名管道和共享内存。

1.4 通信方式

image.png
MySQL使用了半双工通信,所以客户端发送SQL语句给服务端的时候,不管SQL有多大,都是一次发过去的。

比如我们用MyBatis动态SQL生成了一个批量插入的语句,插入10万条数据,values后面跟了一长串的内容,或者 where 条件 in 里面的值太多,会出现问题。这个时候我们必须要调整 MySQL 服务器配置 max_allowed_packet 参数的值(默认是 4M),把它调大,否则就会报错。

对于服务端来说,也是一次性发送所有的数据,不能因为你已经取到了想要的数据就中断操作,这个时候会对网络和内存产生大量消耗。在程序里面避免不带 limit 的这种操作,比如一次把所有满足条件的数据全部查出来,一定要先 count 一下。如果数据量的话,可以分批查询。


2.查询缓存

MySQL 的缓存默认是关闭的。

  1. show variables like 'query_cache%';

MySQL不推荐使用自带的缓存,命中条件过于苛刻。且表里数据发生变化,整张表的缓存全部失效,MySQL8移除掉了缓存。


3.语法解析&预处理

3.1 词法解析

词法分析就是把一个完整的 SQL 语句打碎成一个个的单词。

  1. select name from user where id =1;

它会打碎成 8 个符号,每个符号是什么类型,从哪里开始到哪里结束。

3.2 语法解析

语法分析会对 SQL 做一些语法检查,比如单引号有没有闭合,然后根据 MySQL 定义的语法规则,根据 SQL 语句生成一个数据结构。这个数据结构我们把它叫做解析树(select_lex)。

image.png

任何数据库的中间件,比如 Mycat,Sharding-JDBC(用到了 Druid Parser),都必须要有词法和语法分析功能。

3.3 预处理

如果写了一个词法和语法都正确的 SQL,但是表名或者字段不存在,会在哪里报错?是在数据库的执行层还是解析器?

实际上还是在解析的时候报错,解析 SQL 的环节里面有个预处理器。它会检查生成的解析树,解决解析器无法解析的语义。比如,它会检查表和列名是否存在,检查名字和别名,保证没有歧义。预处理之后得到一个新的解析树。


4.查询优化&查询执行计划

一条SQL语句的执行方式有很多种,但是最终返回的结果都是相同的。查询优化器的目的就是根据解析树生成不同的执行计划(Execution Plan),然后选择一种最优的执行计划,MySQL 里面使用的是基于开销(cost)的优化器,那种执行计划开销最小,就用哪种。

  1. # 查看查询的开销
  2. show status like 'Last_query_cost';

4.1 优化器的作用

  1. 多表联查,以哪张表为基准表
  2. 用不用索引,用哪个索引
  3. 。。。。

4.2 优化器是怎么得到执行计划的

  1. 首先我们要启用优化器的追踪(默认是关闭的)。

开启这开关是会消耗性能的,因为它要把优化分析的结果写到表里面,所以不要轻易开启,或者查看完之后关闭它(改成 off)。

  1. 接着执行一个 SQL 语句,优化器会生成执行计划:

  2. 这个时候优化器分析的过程已经记录到系统表里面了,我们可以查询:

它是一个 JSON 类型的数据,主要分成三部分,准备阶段、优化阶段和执行阶段。

expanded_query 是优化后的 SQL 语句。

considered_execution_plans 里面列出了所有的执行计划。

  1. 分析完记得关掉它

通过追踪优化器,可以看到优化器对sql的初始优化,表的读取顺序,为什么采用了这种读取顺序。为什么采用了某个索引或者采用了全表查询。

4.3 优化器得到的结果

优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。

当然,这个执行计划是不是一定是最优的执行计划呢?不一定,因为 MySQL 也有可能覆盖不到所有的执行计划。

MySQL 提供了一个执行计划的工具。我们在 SQL 语句前面加上 EXPLAIN,就可以看到执行计划的信息。

Explain 的结果也不一定最终执行的方式。

4.4 选错索引

这里错误决定分两类,第一,彻底错误。第二,基于成本最低,但执行速度不是最快。

  1. 由于InnoDB的 MVCC 功能和随机采样方式,默认随机采取几个数据页,当做总体数据。以部分代表整体,本来就有错误的风险。加上数据不断地添加过程中,索引树可能会分裂,结果更加不准确。

    1. 执行 ANALYZE TABLE ,可以重新构建索引,使索引树不过于分裂。
    2. 调整参数,加大InnoDB采样的页数,页数越大越精确,但性能消耗更高。一般不建议这么干。
  2. 在优化阶段,会对表中所有索引进行对比,优化器基于成本的原因,选择成本最低的索引,所以会错过最佳索引。带来的问题便是,执行速度很慢。

    1. 通过explain查看执行计划,结合sql条件查看可以利用哪些索引。
    2. 使用 force index(indexName)强制走指定索引。弊端就是后期若索引名发生改变,或索引被删除,该sql语句需要调整。

5. 存储引擎

得到执行计划以后,SQL 语句是不是终于可以执行了?

  1. 从逻辑的角度来说,我们的数据是放在哪里的,或者说放在一个什么结构里面?

  2. 执行计划在哪里执行?是谁去执行?

表在存储数据的同时,还要组织数据的存储结构,这个存储结构就是由存储引擎决定的,所以也可以把存储引擎叫做表类型。

在 MySQL 里面,支持多种存储引擎,他们是可以替换的,所以叫做插件式的存储引擎。

5.1 查看存储引擎

我们数据库里面已经存在的表,我们怎么查看它们的存储引擎呢?

  1. show table status from `数据库名`;

或者通过 DDL 建表语句来查看。

在 MySQL 里面,我们创建的每一张表都可以指定它的存储引擎,而不是一个数据库只能使用一个存储引擎。存储引擎的使用是以表为单位的。而且,创建表之后还可以修改存储引擎。

一张表使用的存储引擎决定存储数据的结构,那在服务器上它们是怎么存储的呢?先要找到数据库存放数据的路径:
image.png
默认情况下,每个数据库有一个自己文件夹,以 yhd数据库为例。任何一个存储引擎都有一个 frm 文件,这个是表结构定义文件。
image.png
不同的存储引擎存放数据的方式不一样,产生的文件也不一样,innodb 是 1 个,memory 没有,myisam 是两个。

5.2 存储引擎比较

①常见存储引擎

MyISAM 和 InnoDB 是我们用得最多的两个存储引擎,在 MySQL 5.5 版本之前,默认的存储引擎是 MyISAM,它是 MySQL 自带的。

5.5 版本之后默认的存储引擎改成了 InnoDB,最主要的原因还是 InnoDB 支持事务,支持行级别的锁,对于业务一致性要求高的场景来说更适合。

②数据库支持的存储引擎

可以用这个命令查看数据库对存储引擎的支持情况:

  1. show engines ;

其中有存储引擎的描述和对事务、XA 协议和 Savepoints 的支持。

XA 协议用来实现分布式事务(分为本地资源管理器,事务管理器)。

Savepoints 用来实现子事务(嵌套事务)。创建了一个 Savepoints 之后,事务就可以回滚到这个点,不会影响到创建 Savepoints 之前的操作。

image.png

③MyISAM(3 个文件)

应用范围比较小。表级锁定限制了读/写的性能,因此在 Web 和数据仓库配置中,它通常用于只读或以读为主的工作。

特点

  1. 支持表级别的锁(插入和更新会锁表)。不支持事务。

  2. 拥有较高的插入(insert)和查询(select)速度。

  3. 存储了表的行数(count 速度更快)。

适合:只读之类的数据分析的项目。

④InnoDB(2个文件)

mysql 5.7 中的默认存储引擎。InnoDB 是一个事务安全(与 ACID 兼容)的 MySQL存储引擎,它具有提交、回滚和崩溃恢复功能来保护用户数据。InnoDB 行级锁(不升级为更粗粒度的锁)和 Oracle 风格的一致非锁读提高了多用户并发性和性能。InnoDB 将用户数据存储在聚集索引中,以减少基于主键的常见查询的 I/O。为了保持数据完整性,InnoDB 还支持外键引用完整性约束。

特点

  1. 支持事务,支持外键,因此数据的完整性、一致性更高。

  2. 支持行级别的锁和表级别的锁。

  3. 支持读写并发,写不阻塞读(MVCC)。

  4. 特殊的索引存放方式,可以减少 IO,提升查询效率。

适合:经常更新的表,存在并发读写或者有事务处理的业务系统。

⑤Memory(1个文件)

基于内存的存储引擎。

特征:

  • 基于内存的表,服务器重启后,表结构会被保留,但表中的数据会被清空。
  • 不需要进行磁盘IO,比 MYISAM 快了一个数量级。
  • 表级锁,故并发插入性能较低。
  • 每一行是固定的,VARCHAR 列在 memory 存储引擎中会变成 CHAR,可能导致内存浪费。
  • 不支持 BLOB 或 TEXT 列,如果sql返回的结果列中包含 BLOB 或 TEXT,就直接采用 MYISAM 存储引擎,在磁盘上建临时表
  • 支持哈希索引,B+树索引

MEMORY 存储引擎在很多地方可以发挥很好的作用:

  • 用于查找或映射表,例如邮编和州名的映射表
  • 用于缓存周期性聚合数据的结果
  • 用于保存数据分析中产生的中间结果。即SQL执行过程中用到的临时表
  • 监控MySQL内存中的执行情况,例如:information_schema 库下的表基本都是 memory 存储引擎,监控InnoDB缓冲池中page(INNODB_BUFFER_PAGE表),InnoDB缓冲池状态(INNODB_BUFFER_POOL_STATS表)、InnoDB缓存页淘汰记录(INNODB_BUFFER_PAGE_LRU表)、InnoDB锁等待(INNODB_LOCK_WAITS表)、InnoDB锁信息(INNODB_LOCKS表)、InnoDB中正在执行的事务(INNODB_TRX表)等。

MEMORY 存储引擎默认 hash 索引,故等值查询特别快。同时也支持B+树索引。虽然查询速度特别快,但依旧无法取代传统的磁盘建表。

⑥CSV(3个文件)

它的表实际上是带有逗号分隔值的文本文件。csv表允许以csv格式导入或转储数据,以便与读写相同格式的脚本和应用程序交换数据。因为 csv 表没有索引,所以通常在正常操作期间将数据保存在 innodb 表中,并且只在导入或导出阶段使用 csv 表。

特点

不允许空行,不支持索引。格式通用,可以直接编辑,适合在不同数据库之间导入导出。

5.3 如何选择存储引擎

  1. 如果对数据一致性要求比较高,需要事务支持,可以选择 InnoDB。

  2. 如果数据查询多更新少,对查询性能要求比较高,可以选择 MyISAM。

  3. 如果需要一个用于查询的临时表,可以选择 Memory。


6.执行引擎

执行引擎,它利用存储引擎提供的相应的 API 来完成操作。

为什么我们修改了表的存储引擎,操作方式不需要做任何改变?因为不同功能的存储引擎实现的 API 是相同的。

最后把数据返回给客户端,即使没有结果也要返回。


二,一条SQL的更新流程

更新和查询很多地方并没有区别,仅仅在于拿到数据之后的操作。
image.png

1.内存结构

InnnoDB 的数据都是放在磁盘上的,InnoDB 操作数据有一个最小的逻辑单位,叫做页(索引页和数据页)。我们对于数据的操作,不是每次都直接操作磁盘,因为磁盘的速度太慢了。InnoDB 使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。这个内存区域就叫 Buffer Pool。

下一次读取相同的页,先判断是不是在缓冲池里面,如果是,就直接读取,不用再次访问磁盘。

修改数据的时候,先修改缓冲池里面的页。内存的数据页和磁盘数据不一致的时候,我们把它叫做脏页。InnoDB 里面有专门的后台线程把 Buffer Pool 的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。

Buffer Pool 是 InnoDB 里面非常重要的一个结构,主要分为 3 个部分: Buffer Pool、Change Buffer、Adaptive HashIndex,另外还有一个(redo)log buffer。

1.1 buffer pool

Buffer Pool 缓存的是页信息,包括数据页、索引页,默认大小是 128M(134217728 字节),可以调整。

查看服务器状态,里面有很多跟 Buffer Pool 相关的信息:

  1. SHOW STATUS LIKE '%innodb_buffer_pool%';

查看参数(系统变量):

  1. SHOW VARIABLES like '%innodb_buffer_pool%';

内存的缓冲池写满了怎么办?(Redis 设置的内存满了怎么办?)InnoDB 用 LRU算法来管理缓冲池(链表实现,不是传统的 LRU,分成了 young 和 old),经过淘汰的数据就是热点数据。

内存缓冲区对于提升读写性能有很大的作用。当需要更新一个数据页时,如果数据页在 Buffer Pool 中存在,那么就直接更新好了。否则的话就需要从磁盘加载到内存,再对内存的数据页进行操作。也就是说,如果没有命中缓冲池,至少要产生一次磁盘 IO。

1.2 ChangeBuffer写缓冲

如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(Insert、Delete、Update)的执行速度。

这一块区域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入缓冲,现在也能支持 delete 和 update。

最后把 Change Buffer 记录到数据页的操作叫做 merge。什么时候发生 merge?有几种情况:在访问这个数据页的时候,或者通过后台线程、或者数据库 shut down、redo log 写满时触发。

如果数据库大部分索引都是非唯一索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用 Change Buffer(写缓冲)。写多读少的业务,调大这个值:

  1. SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';

代表 Change Buffer 占 Buffer Pool 的比例,默认 25%。

1.3 Adaptive Hash Index

当我们需要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool中,如果该页已经在Buffer Pool中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在Buffer Pool中呢?

我们其实是根据表空间号 + 页号来定位一个页的,也就相当于表空间号 + 页号是一个key缓存页就是对应的value,怎么通过一个key来快速找着一个value呢?那肯定是哈希表。

所以我们可以用表空间号 + 页号作为key缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

1.4 (redo)Log Buffer

如果 Buffer Pool 里面的脏页还没有刷入磁盘时,数据库宕机或者重启,这些数据丢失。如果写操作写到一半,甚至可能会破坏数据文件导致数据库不可用。为了避免这个问题,InnoDB 把所有对页面的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现 crash-safe)——用它来实现事务的持久性。

image.png
这个文件就是磁盘的 redo log(叫做重做日志),对应于/var/lib/mysql/目录下的ib_logfile0 和 ib_logfile1,每个 48M。这 种 日 志 和 磁 盘 配 合 的 整 个 过 程 , 其 实 就 是 MySQL 里 的 WAL 技 术(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。

  1. show variables like 'innodb_log%';
含义
innodb_log_file_size 指定每个文件的大小,默认 48M
innodb_log_files_in_group 指定文件的数量,默认为 2
innodb_log_group_home_dir 指定文件所在路径,相对或绝对。如果不指定,则为datadir 路径。

同样是写磁盘,为什么不直接写到 db file 里面去?为什么先写日志再写磁盘?

磁盘的最小组成单元是扇区,通常是 512 个字节。操作系统和内存打交道,最小的单位是页 Page。操作系统和磁盘打交道,读写磁盘,最小的单位是块 Block。

如果我们所需要的数据是随机分散在不同页的不同扇区中,那么找到相应的数据需要等到磁臂旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,依次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。

假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。

刷盘是随机 I/O,而记录日志是顺序 I/O,顺序 I/O 效率更高。因此先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐。

当然 redo log 也不是每一次都直接写入磁盘,在 Buffer Pool 里面有一块内存区域(Log Buffer)专门用来保存即将要写入日志文件的数据,默认 16M,它一样可以节省磁盘 IO。

image.png

  1. SHOW VARIABLES LIKE 'innodb_log_buffer_size';

redo log 的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自 buffer pool。redo log 写入磁盘,不是写入数据文件。

那么,Log Buffer 什么时候写入 log file?

在我们写入数据到磁盘的时候,操作系统本身是有缓存的。flush 就是把操作系统缓冲区写入到磁盘。

log buffer 写入磁盘的时机,由一个参数控制,默认是 1。

  1. SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
含义
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 有什么特点?

  1. redo log 是 InnoDB 存储引擎实现的,并不是所有存储引擎都有。

  2. 不是记录数据页更新之后的状态,而是记录这个页做了什么改动,属于物理日志。(redo log 记录的是执行的结果)

  3. redo log 的大小是固定的,前面的内容会被覆盖。

image.png
check point 是当前要覆盖的位置。如果 write pos 跟 check point 重叠,说明 redolog 已经写满,这时候需要同步 redo log 到磁盘中。

这是 MySQL 的内存结构,总结一下,分为:Buffer pool、change buffer、Adaptive Hash Index、 log buffer。

磁盘结构里面主要是各种各样的表空间,叫做 Table space。

1.5 缓存的疑问

缓存(cache)是在读取硬盘中的数据时,把最常用的数据保存在内存的缓存区中,再次读取该数据时,就不去硬盘中读取了,而在缓存中读取。缓冲(buffer)是在向硬盘写入数据时,先把数据放入缓冲区,然后再一起向硬盘写入,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。

然后,InnoDB架构中,有非常重要的一个部分——缓冲池。该缓冲池需要占用服务器内存,且专用于MySQL的服务器,建议把80%的内存交给MySQL。

缓冲池有一个缓存的功能。这个缓存,是InnoDB自带的,而且经常会用到。该缓存功能并不是MySQL架构中的缓存组件。这是两者最大的区别。

  • MySQL组件中的缓存
    1. 所处位置:MySQL架构中的缓存组件
    2. 缓存内容:缓存的是SQL 和 该SQL的查询结果。如果SQL的大小写,格式,注释不一致,则被认为是不同的SQL,重新查询数据库,并缓存一份数据。
    3. 可否关闭:是可以手动关闭,并卸载该组件的。
  • InnoDB中的缓存
    1. 所处位置:InnoDB架构中的缓冲池
    2. 缓存内容:缓存的是所有需要查找的数据,所在的数据页。
    3. 可否关闭:是InnoDB缓冲池自带的功能,无法关闭,无法卸载。如果InnoDB的缓冲池被关闭或卸载,则InnoDB直接瘫痪。所以说缓冲池是InnoDB的最重要的一部分。

不建议使用MySQL的缓存是指,不建议使用MySQL架构中的缓存组件,并不是同时否定了InnoDB中的缓存功能。


2.磁盘结构

表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。InnoDB 的表空间分为 5 大类。

2.1 系统表空间

在默认情况下 InnoDB 存储引擎有一个共享表空间(对应文件/var/lib/mysql/ibdata1),也叫系统表空间。

InnoDB 系统表空间包含 InnoDB 数据字典和双写缓冲区,(Change Buffer 和 UndoLogs),如果没有指定 file-per-table,也包含用户创建的表和索引数据。

  1. undo 在后面介绍,因为有独立的表空间。

  2. 数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。

  3. 双写缓冲(InnoDB 的一大特性)

InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为 16K,操作系统页大小为 4K,InnoDB 的页写入到磁盘时,一个页需要分 4 次写。

image.png

如果存储引擎正在写入页的数据到磁盘时发生了宕机,可能出现页只写了一部分的情况,比如只写了 4K,就宕机了,这种情况叫做部分写失效(partial page write),可能会导致数据丢失。

  1. show variables like 'innodb_doublewrite';

如果这个页本身已经损坏了,用它来做崩溃恢复是没有意义的。所以在对于应用 redo log 之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再应用 redo log。这个页的副本就是 double write,InnoDB 的双写技术。通过它实现了数据页的可靠性。

跟 redo log 一样,double write 由两部分组成,一部分是内存的 double write,一个部分是磁盘上的 double write。因为 double write 是顺序写入的,不会带来很大的开销。

在MySQL5.7之前,所有的表共享一个系统表空间,这个文件会越来越大,而且它的空间不会收缩。

2.2 独占表空间

我们可以让每张表独占一个表空间。这个开关通过 innodb_file_per_table 设置,默认开启。

  1. SHOW VARIABLES LIKE 'innodb_file_per_table';

开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的 ibd 文件,存放表的索引和数据。但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。

2.3 通用表空间

通用表空间也是一种共享的表空间,跟 ibdata1 类似。

可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义。语法:

  1. create tablespace ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb;

在创建表的时候可以指定表空间,用 ALTER 修改表空间可以转移表空间。

  1. create table t2673(id integer) tablespace ts2673;

不同表空间的数据是可以移动的。删除表空间需要先删除里面的所有表:

  1. drop table t2673;
  2. drop tablespace ts2673;

2.4 临时表空间

存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。

memory向template的过渡,还有磁盘上简历临时表用的什么存储引擎?

8.0之前,内存临时表用Memory引擎创建,但假如字段中有BLOB或TEXT,或结果太大,就会转用MYISM在磁盘上建表,8.0之后内存临时表由MEMORY引擎更改为TempTable引擎,相比于前者,后者支持以变长方式存储VARCHAR,VARBINARY等变长字段。从MySQL 8.0.13开始,TempTable引擎支持BLOB字段。如果超过内存表大小,则用InnoDB建表。

2.5 redo log

2.6 undo log 表空间

undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括 select)。

如果修改数据时出现异常,可以用 undo log 来实现回滚操作(保持原子性)。

在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,属于逻辑格式的日志(记录操作)。

redo Log 和 undo Log 与事务密切相关,统称为事务日志。

undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收缩,也可以单独创建一个 undo 表空间。

  1. show global variables like '%undo%';

2.7 一条SQL的更新流程

  1. # id =1 的记录原 name = 'yhd'
  2. update user set name = '二十' where id=1;
  1. 事务开始,从内存或者磁盘取到这条数据,返回给server的执行器
  2. 执行器修改这一行数据的值为二十
  3. 记录name =yhd 到undo log
  4. 记录name = 二十 到redo log
  5. 调用存储引擎接口,在buffer pool 中修改 name =二十
  6. 事务提交

内存和磁盘之间,工作着很多后台线程。


3.后台线程

image.png
后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为:master线程,IO 线程,purge 线程,page cleaner 线程。

3.1 Master 线程

Master Thread是InnoDB存储引擎非常核心的一个后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。

  1. void master_thread()
  2. {
  3. loop:
  4. for(int i = 0; i < 10; ++i){
  5. thread_sleep(1); // sleep 1秒
  6. do log buffer flush to disk;
  7. if(last_one_second_ios < 5)
  8. do merge at most 5 insert buffer;
  9. if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
  10. // 如果缓冲池中的脏页比例大于innodb_max_dirty_pages_pct(默认是75时)
  11. do buffer pool flush 100 dirty page; // 刷新100脏页到磁盘
  12. if(no user activity)
  13. goto backgroud loop;
  14. }
  15. if(last_ten_second_ios < 200) // 如果过去10内磁盘IO次数小于设置的innodb_io_capacity的值(默认是200)
  16. do buffer pool flush 100 dirty page;
  17. do merge at most 5 insert buffer; // 合并插入缓冲是innodb_io_capacity的5%(10)(总是)
  18. do log buffer flush to disk;
  19. do full purge;
  20. if(buf_get_modified_ratio_pct > 70%)
  21. do buffer pool flush 100 dirty page;
  22. else
  23. buffer pool flush 10 dirty page;
  24. backgroud loop // 后台循环
  25. do full purge // 删除无用的undo页 (总是)
  26. do merge 20 insert buffer; // 合并插入缓冲是innodb_io_capacity的5%(10)(总是)
  27. if not idle // 如果不空闲,就跳回主循环,如果空闲就跳入flush loop
  28. goto loop: // 跳到主循环
  29. else
  30. goto flush loop
  31. flush loop: // 刷新循环
  32. do buffer pool flush 100 dirty page;
  33. if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
  34. // 如果缓冲池中的脏页比例大于innodb_max_dirty_pages_pct的值(默认75%)
  35. goto flush loop; // 跳到刷新循环,不断刷新脏页,直到符合条件
  36. goto suspend loop; // 完成刷新脏页的任务后,跳入suspend loop
  37. suspend loop:
  38. suspend_thread(); //master线程挂起,等待事件发生
  39. waiting event;
  40. goto loop;
  41. }

Master Thread具有最高的线程优先级别。内部由多个循环组成:主循环(loop)后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。Master Thread会根据数据库运行的状态在loop、backgroup loop、flush loop和suspend loop中进行切换。loop是主循环,大多数的操作都在这个循环中,主要有两大部分的操作——每秒钟的操作和每10秒钟的操作。

①每秒钟的操作

  1. 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
    即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这也解释了为什么再大的事务提交的时间也是很短的。
  2. 合并插入缓冲(可能)
    合并插入缓冲并不是每秒都会发生的。InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB存储引擎认为当前的IO压力很小,可以执行合并插入缓冲的操作;
  3. 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能); 刷新100个脏页也不是每秒都会发生的,InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中 innodb_max_dirty_pages_pct这个参数(默认是75,代表75%),如果超过了这个值,InnoDB存储引擎则认为需要做磁盘同步的操作,将100个脏页写入磁盘中。
  4. 如果当前没有用户活动,则切换到background loop(可能)。


②每十秒的操作

  1. 刷新100个脏页到磁盘(可能) InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次,如果是,InnoDB存储引擎认为当前有足够的磁盘IO能力,因此将100个脏页刷新到磁盘。
  2. 合并至多5个插入缓冲(总是)
  3. 将日志缓冲刷新到磁盘(总是)
  4. 删除无用的Undo页(总是)
  5. 刷新100个或者10个脏页到磁盘(总是) InnoDB存储引擎会执行full purge操作,即删除无用的Undo页。对表进行update,delete这类的操作时,原先的行被标记为删除,但是因为一致性读的关系,需要保留这些行版本的信息。但是在full purge过程中,InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以删除,InnoDB存储引擎会立即将其删除。从源代码中可以看出,InnoDB存储引擎在执行full purge 操作时,每次最多尝试回收20个undo页。
    然后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。

如果当前没有用户活动(数据库空闲)或者数据库关系,就会切换到backgroud loop这个循环。 backgroud loop会执行以下操作:

  1. 删除无用的Undo页(总是)
  2. 合并20个插入缓冲(总是)
  3. 跳回到主循环(总是)
  4. 不断刷新100个页直到符合条件(可能,需要跳转到flush loop中完成)

如果flush loop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspend_loop,将Master Thread挂起,等待事件的发生。若用户启用了InnoDB存储引擎,却没有使用任何InnoDB存储引擎的表,那么Master Thread总是处于挂起的状态。

1.0.x版本中,InnoDB存储引擎最多只会刷新100个脏页到磁盘,合并20个插入缓冲。如果是在写入密集的应用程序中,每秒可能会产生大于100个的脏页,如果是产生大于20个插入缓冲的情况,那么可能会来不及刷新所有的脏页以及合并插入缓冲。后来,InnoDB存储引擎提供了参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值为200。

对于刷新到磁盘的页的数量,会按照innodb_io_capacity的百分比来进行控制。规则如下:

  1. 在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值的5%;
  2. 在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity;

如果用户使用的是SSD类的磁盘,可以将innodb_io_capacity的值调高,直到符合磁盘IO的吞吐量为止;

另一个问题是参数innodb_max_dirty_pages_pct的默认值,在1.0.x版本之前,该值的默认值是90,意味着脏页占缓冲池的90%。InnoDB存储引擎在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb_max_dirty_pages_pct,才会刷新100个脏页,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。 后来将innodb_max_dirty_pages_pct的默认值改为了75。这样既可以加快刷新脏页的频率,又能够保证磁盘IO的负载。

还有一个新的参数是innodb_adaptive_flushing(自适应地刷新),该值影响每秒刷新脏页的数量。原来的刷新规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页;大于innodb_max_dirty_pages_pct时,刷新100个脏页。随着innodb_adaptive_flushing参数的引入,InnoDB通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。buf_flush_get_desired_flush_rate函数通过判断产生重做日志的速率来决定最合适的刷新脏页数量。

之前每次进行full purge 操作时,最多回收20个Undo页,从InnoDB 1.0.x版本开始引入了参数innodb_purge_batch_size,该参数可以控制每次full purge回收的Undo页的数量。该参数的默认值为20,并可以动态地对其进行修改。

1.2.x版本中再次对Master Thread进行了优化,对于刷新脏页的操作,从Master Thread线程分离到一个单独的Page Cleaner Thread,从而减轻了Master Thread的工作,同时进一步提高了系统的并发性。

3.2 IO 线程

InnoDB中大量使用AIO (Async IO) 来处理IO请求。IO Thread的作用,是负责这些 IO 请求的回调(call back)。

image.png

3.3 Purge 线程

事务被提交后,其所使用的undo log可能不在需要。因此,需要purge thread来回收已经使用并分配的undo页。以前Master Thread来完成释放undo log,InnoDB1.1独立出来,分担主线程压力。

3.4 Page Cleaner 线程

负责将脏页刷新到磁盘。以前Master Thread来刷新脏页,InnoDB1.2独立出来,分担主线程压力。

除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件,叫做binlog,它可以被所有的存储引擎使用。


4.binlog

binlog 以事件的形式记录了所有的DDL 和DML 语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复。跟redo log不一样,它的文件内容是可以追加的,没有固定大小限制。在开启了 binlog 功能的情况下,我们可以把 binlog 导出成 SQL 语句,把所有的操作重放一遍,来实现数据的恢复。binlog 的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的 binlog,然后执行一遍。

有了这两个日志之后,来看一下一条更新语句是怎么执行的:

  1. # id =1 的记录原 name = 'yhd'
  2. update user set name = '二十' where id=1;
  1. 事务开始,从内存或者磁盘取到这条数据所在的数据页,返回给server的执行器
  2. 执行器修改这一行数据的值为二十
  3. 记录name =yhd 到undo log
  4. 在buffer pool 中修改 name =二十,此时该页变成脏页
  5. 记录name = 二十 到redo log buffer,redo log buffer每秒刷盘。
  6. redo log 进入prepare状态,然后告诉执行器,执行完成了,可以随时提交
  7. 写入binlog
  8. 事务提交,并回写最终状态到redo log里,代表该事务已经提交

事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便不断写入redo log文件中。一般情况下,每次事务commit时,必须调用 fsync 操作,将redo日志缓冲同步写到磁盘。另外,每次事务提交时同步写到磁盘bin log中。

那么就有了一个谁先谁后的问题:redo log 先,bin log 后。

两阶段提交的内容:事务提交时,redo log处于 pre状态 -> 写入bin log -> 事务真正提交。

当发生崩溃恢复时,查看的是bin log是否完整,如果bin log完整,则代表事务已经提交。

如果在两阶段提交过程中,bin log写入失败,则事务无法终止提交,崩溃恢复时就不需要重做。如果bin log写完的一瞬间,服务器宕机了,事务都来不及提交,此时bin log并不是完整的,缺少了最终的commit标记。因此也是提交失败。

简单说,redo log和bin log都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

1.png


三,MySQL中支持的字符集和排序规则

1.MySQL中的utf8和utf8mb4


utf8字符集表示一个字符需要使用1~4个字节,但是我们常用的一些字符使用1~3个字节就可以表示了。而在MySQL中字符集表示一个字符所用最大字节长度在某些方面会影响系统的存储和性能,所以设计MySQL的大叔偷偷的定义了两个概念:

  • utf8mb3:阉割过的utf8字符集,只使用1~3个字节表示字符。
  • utf8mb4:正宗的utf8字符集,使用1~4个字节表示字符。

在MySQL中utf8是utf8mb3的别名,所以之后在MySQL中提到utf8就意味着使用1~3个字节来表示一个字符,如果有使用4字节编码一个字符的情况,比如存储一些emoji表情啥的,那请使用utf8mb4。

查看字符集:SHOW (CHARACTER SET|CHARSET)

2.字符集&比较规则的应用

2.1 各级别的字符集和比较规则


MySQL有4个级别的字符集和比较规则,分别是:

  • 服务器级别
  • 数据库级别
  • 表级别
  • 列级别

接下来仔细看一下怎么设置和查看这几个级别的字符集和比较规则。

服务器级别

  1. mysql> SHOW VARIABLES LIKE 'character_set_server';
  2. +----------------------+-------+
  3. | Variable_name | Value |
  4. +----------------------+-------+
  5. | character_set_server | utf8 |
  6. +----------------------+-------+
  7. 1 row in set (0.00 sec)
  8. mysql> SHOW VARIABLES LIKE 'collation_server';
  9. +------------------+-----------------+
  10. | Variable_name | Value |
  11. +------------------+-----------------+
  12. | collation_server | utf8_general_ci |
  13. +------------------+-----------------+
  14. 1 row in set (0.00 sec)

可以在启动服务器程序时通过启动选项或者在服务器程序运行过程中使用SET语句修改这两个变量的值。比如我们可以在配置文件中这样写:

  1. [server]
  2. character_set_server=gbk
  3. collation_server=gbk_chinese_ci

当服务器启动的时候读取这个配置文件后这两个系统变量的值便修改了。

数据库级别

我们在创建和修改数据库的时候可以指定该数据库的字符集和比较规则,具体语法如下:

  1. CREATE DATABASE 数据库名
  2. [[DEFAULT] CHARACTER SET 字符集名称]
  3. [[DEFAULT] COLLATE 比较规则名称];
  4. ALTER DATABASE 数据库名
  5. [[DEFAULT] CHARACTER SET 字符集名称]
  6. [[DEFAULT] COLLATE 比较规则名称];

其中的DEFAULT可以省略,并不影响语句的语义。比方说我们新创建一个名叫charset_demo_db的数据库,在创建的时候指定它使用的字符集为gb2312,比较规则为gb2312_chinese_ci:

  1. mysql> CREATE DATABASE charset_demo_db
  2. -> CHARACTER SET gb2312
  3. -> COLLATE gb2312_chinese_ci;
  4. Query OK, 1 row affected (0.01 sec)

查看

  1. mysql> USE charset_demo_db;
  2. Database changed
  3. mysql> SHOW VARIABLES LIKE 'character_set_database';
  4. +------------------------+--------+
  5. | Variable_name | Value |
  6. +------------------------+--------+
  7. | character_set_database | gb2312 |
  8. +------------------------+--------+
  9. 1 row in set (0.00 sec)
  10. mysql> SHOW VARIABLES LIKE 'collation_database';
  11. +--------------------+-------------------+
  12. | Variable_name | Value |
  13. +--------------------+-------------------+
  14. | collation_database | gb2312_chinese_ci |
  15. +--------------------+-------------------+
  16. 1 row in set (0.00 sec)
  17. mysql>

可以看到这个charsetdemo_db数据库的字符集和比较规则就是我们在创建语句中指定的。需要注意的一点是: character_set_databasecollation_database_ 这两个系统变量是只读的,我们不能通过修改这两个变量的值而改变当前数据库的字符集和比较规则。

表级别

我们也可以在创建和修改表的时候指定表的字符集和比较规则,语法如下:

  1. CREATE TABLE 表名 (列的信息)
  2. [[DEFAULT] CHARACTER SET 字符集名称]
  3. [COLLATE 比较规则名称]]
  4. ALTER TABLE 表名
  5. [[DEFAULT] CHARACTER SET 字符集名称]
  6. [COLLATE 比较规则名称]

比方说我们在刚刚创建的charset_demo_db数据库中创建一个名为t的表,并指定这个表的字符集和比较规则:

  1. mysql> CREATE TABLE t(
  2. -> col VARCHAR(10)
  3. -> ) CHARACTER SET utf8 COLLATE utf8_general_ci;
  4. Query OK, 0 rows affected (0.03 sec)

如果创建和修改表的语句中没有指明字符集和比较规则,将使用该表所在数据库的字符集和比较规则作为该表的字符集和比较规则。

列级别

需要注意的是,对于存储字符串的列,同一个表中的不同的列也可以有不同的字符集和比较规则。我们在创建和修改列定义的时候可以指定该列的字符集和比较规则,语法如下:

  1. CREATE TABLE 表名(
  2. 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
  3. 其他列...
  4. );
  5. ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];

比如我们修改一下表t中列col的字符集和比较规则可以这么写:

  1. mysql> ALTER TABLE t MODIFY col VARCHAR(10) CHARACTER SET gbk COLLATE gbk_chinese_ci;
  2. Query OK, 0 rows affected (0.04 sec)
  3. Records: 0 Duplicates: 0 Warnings: 0
  4. mysql>

对于某个列来说,如果在创建和修改的语句中没有指明字符集和比较规则,将使用该列所在表的字符集和比较规则作为该列的字符集和比较规则。

在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示会发生错误。比方说原先列使用的字符集是utf8,列中存储了一些汉字,现在把列的字符集转换为ascii的话就会出错,因为ascii字符集并不能表示汉字字符。

2.2 客户端和服务器通信中的字符集

编码和解码使用的字符集不一致的后果


如果对于同一个字符串编码和解码使用的字符集不一样,会产生意想不到的结果,作为人类的我们看上去就像是产生了乱码一样。

从发送请求到接收结果过程中发生的字符集转换

  • 客户端使用操作系统的字符集编码请求字符串,向服务器发送的是经过编码的一个字节串。
  • 服务器将客户端发送来的字节串采用character_set_client代表的字符集进行解码,将解码后的字符串再按照character_set_connection代表的字符集进行编码。
  • 如果character_set_connection代表的字符集和具体操作的列使用的字符集一致,则直接进行相应操作,否则的话需要将请求中的字符串从character_set_connection代表的字符集转换为具体操作的列使用的字符集之后再进行操作。
  • 将从某个列获取到的字节串从该列使用的字符集转换为character_set_results代表的字符集后发送到客户端。
  • 客户端使用操作系统的字符集解析收到的结果集字节串。

image.png

在这个过程中各个系统变量的含义如下:

系统变量 描述
character_set_client 服务器解码请求时使用的字符集
character_set_connection 服务器处理请求时会把请求字符串从character_set_client转为character_set_connection
character_set_results 服务器向客户端返回数据时使用的字符集

一般情况下要使用保持这三个变量的值和客户端使用的字符集相同。

比较规则的作用通常体现比较字符串大小的表达式以及对某个字符串列进行排序中。