引言
MySQL作为全球最流行的开源关系型数据库,其架构设计融合了精巧的工程智慧。本文将从架构设计的角度深入解析MySQL的核心组件和工作原理,通过剖析逻辑架构、日志文件、存储引擎实现等关键技术点,帮助开发者构建完整的MySQL知识体系。
1、MySQL逻辑架构图
(图示:MySQL经典分层架构)
-
连接池:负责跟客户端建立连接,管理用户的连接,监听并且接收用户的请求。
-
系统管理和控制工具 :系统管理和控制工具,例如备份恢复、Mysql复制、集群等。
-
SQL接口:接受用户的SQL命令,并且返回SQL执行结果。
-
解析器: SQL传递到解析器的时候会被解析器验证和解析,SQL词法解析和语法解析。
-
查询优化器:对SQL语句进行优化,使用explain可查看SQL的执行计划。
-
缓存:对查询结果进行缓存,MySQL8.0已移除,不建议使用。
-
存储引擎:存储引擎就是存取数据、建立与更新索引、查询数据等技术的实现方法。
-
系统文件: 将数据存储到文件中,并且与存储引擎交互。
2.MySQL连接器
MySQL连接器负责建立和管理用户连接,下面介绍一些和连接器相关的查询语句。
安装好MySQL后,使用如下命令建立连接:
css
mysql -h {ip} -P {port} -u {user} -p {password}
使用 show processlist 查看当前MySQL的连接(如图),可以看到有两条sleep状态的连接,MySQL默认设置的是8小时
查看服务支持最大连接数:
sql
show variables like '%max_connections%';
查看空闲连接(连接MySQL后未执行命令)最大空闲时长(默认8小时):
sql
show variables like 'wait_timeout';
查看服务器响应的最大连接数:
sql
show global status like 'Max_used_connections';
3、MySQL日志文件
MySQL是通过文件系统对数据索引后进行存储的,MySQL从物理结构上可以分为日志文件和数据及索引文件。MySQL在Linux中的数据索引文件和日志文件通常放在/var/lib/mysql目录下。MySQL通过日志记录了数据库操作信息和错误信息。常用日志文件如下:
- 错误日志:/var/log/mysql-error.log
- 查询日志:general_query.log
- 慢查询日志:slow_query_log.log
- 二进制日志:/var/lib/mysql/mysql-bin
- 事务重做日志:redo log
- 中继日志:relay log
- 回滚日志:undo log
可以通过命令查看当前数据库中的日志使用信息:
sql
show variables like 'log_%';
3.1 错误日志
默认开启,错误日志记录了运行过程中遇到的所有严重的错误信息,以及 MySQL每次启动和关闭的详细信息。错误日志所记录的信息是可以通过log_error和log_warnings配置来定义的。从5.5.7以后不能关 闭错误日志。
如何配置:
- log_error:指定错误日志存储位置
- log-warnings:是否将警告信息输出到错误日志中。
- log_warnings 为0, 表示不记录告警信息。
- log_warnings 为1, 表示告警信息写入错误日志。
- log_warnings 大于1, 表示各类告警信息,例如:有关网络故障的信息和重新连接信息写入错误日志。
示范:
ini
log_error=/var/log/mysql-error.log
log_warnings=2
3.2 通用查询日志
默认关闭。由于通用查询日志会记录用户的所有操作,其中还包含增删查改等信息,在并发操作大的环 境下会产生大量的信息从而导致不必要的磁盘IO,会影响MySQL的性能的。 如果不是为了调试数据库,不建议开启查询日志。
3.3 慢查询日志
默认关闭,通过以下设置开启。记录执行时间超过long_query_time秒的所有查询,便于收集查询时间 比较长的SQL语句。
查看慢查询信息:
sql
# 查询是否开启和存储地址
show variables like '%slow_query%';
慢查询阈值查询:
sql
show variables like 'long_query_time%';
配置慢查询开启:
ini
# 开启慢查询日志
slow_query_log=ON
# 慢查询的阈值,单位秒
long_query_time=10
# 日志记录文件
# 如果没有给出file_name值, 默认为主机名,后缀为-slow.log。
# 如果给出了文件名,但不是绝对路径名,文件则写入数据目录。
slow_query_log_file=slow_query_log.log
3.4 二进制日志
默认关闭。binlog是server层的日志,追加写无大小限制。binlog记录了数据库所有的ddl语句和dml语句,但不包括 select语句内容,语句以事件的形式保存,描述了数据的变更顺序,binlog还包括了每个更新语句的执行 时间信息。如果是DDL语句,则直接记录到binlog日志,而DML语句,必须通过事务提交才能记录到 binlog日志中。
binlog主要用于实现mysql 主从复制 、数据备份 、数据恢复。
3.5 事务重做日志
redo log是InnoDB存储引擎层的日志(固定大小),通过WAL机制保证数据安全的同时,提高数据库更新性能。
由于篇幅问题, binlog和redo log详细的原理就不展开了, 可参考:
02 | 日志系统:一条SQL更新语句是如何执行的?-MySQL 实战 45 讲-极客时间
3.6 回滚日志
undo log是innodb引擎层的日志,在事务的修改记录之前,会把该记录的原值先保存到undo log后,再做修改,以便修改过程中出错能够恢复原值或者其他的事务读取。
作用:
- MySQL mvcc中,通过undo log链 + readview,实现MySQL中的RC和RR隔离级别。
- 事务rollback过程中,使用undo log恢复数据。
3.7 中继日志
中继日志relay log
只在主从服务器架构的从服务器上存在。 从服务器slave
为了与主服务器Master
保持一致,要从主服务器读取二进制日志binlog
的内容,并且把读取到的信息写入本地的日志文件中,这个slave
服务器本地的日志文件叫做中继日志。
4、MySQL数据文件
查看MySQL数据文件地址:
sql
show variables like '%datadir%';
ibdata文件:使用系统表空间存储表数据和索引信息,所有表共同使用一个或者多个ibdata文件。
InnoDB存储引擎的数据文件:
- .frm文件:主要存放与表相关的数据信息,主要包括表结构的定义信息
- .ibd:使用独享表空间存储表数据和索引信息,一张表对应一个ibd文件。
MyISAM存储引擎的数据文件:
- .frm文件:主要存放与表相关的数据信息,主要包括表结构的定义信息
- .myd文件:主要用来存储表数据信息。
- .myi文件:主要用来存储表数据文件中任何索引的数据树。
5、一条SQL语句完整的执行流程
5.1 第一步:连接到数据库
使用命令连接到数据:
css
-- 连接命令 mysql -h127.0.0.1 -P3306 -uroot -p
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态。客户端如果太长时间没动静,连接器 就会自动将它断开。这个时间是由参数 wait_timeout 控制的默认值是8小时, 这个前面连接器已讲过不再赘述。
5.2 第二步:查询缓存
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及 其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句hash之后的值,value 是查询的结果。
- 如果你的查询语句在缓存中,会被直接返回给客户端。
- 如果语句不在查询缓存中,就会继续后面的执行阶段。
执行完成后,执行结果会被存入查询缓存 中。 如果查询命中缓存,MySQL 不需要执行后面的复杂操作就可以直接返回结果,效率会很高!但是不建议 使用MySQL的内置缓存功能
查询缓存默认是关闭的状态:
perl
# 查看是否开启缓存
show variables like 'query_cache_type';
# 查看缓存的命中次数:
show status like 'qcache_hits';
# 开启缓存
在/etc/my.cnf文件中修改"query_cache_type"参数
1.值为`0或OFF`会禁止使用缓存。
2.值为`1或ON`将启用缓存,但以`SELECT SQL_NO_CACHE`开头的语句除外。
3.值为`2或DEMAND`时,只缓存以`SELECT SQL_CACHE`开头的语句。
清空查询缓存
ini
# 清理查询缓存内存碎片。
FLUSH QUERY CACHE;
# 从查询缓存中移出所有查询。
RESET QUERY CACHE;
# 关闭所有打开的表,同时该操作将会清空查询缓存中的内容。
FLUSH TABLES;
为什么不建议使用MySQL的查询缓存?
因为查询缓存往往弊大于利
- 成本高:查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。
- 命中率不高:对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一 张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
- 功能并不如专业的缓存工具更好:redis、memcache、ehcache等
好在 MySQL 也提供了这种按需使用的方式。你可以将参数 query_cache_type 设置成 DEMAND,这样 对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显 式指定,像下面这个语句一样:
csharp
select sql_cache * from city where city_id = 1;
注意:MySQL8.0已经移除缓存模块。
5.3 第三步:解析SQL语句
如果查询缓存没有命中,接下来就需要进入正式的查询阶段了。客户端程序发送过来的请求,实际上只 是一个字符串而已,所以MySQL服务器程序首先需要对这个字符串做分析,判断请求的语法是否正确, 然后从字符串中将要查询的表、列和各种查询条件都提取出来,本质上是对一个SQL语句编译的过程, 涉及词法解析、语法分析、预处理器等。
- 词法分析:词法分析就是把一个完整的 SQL 语句分割成一个个的字符串
- 语法分析:语法分析器根据词法分析的结果做语法检查,判断你输入的SQL 语句是否满足 MySQL 语法。
- 预处理器:预处理器则会进一步去检查解析树是否合法,比如表名是否存在,语句中表的列是否存 在等等,在这一步MySQL会检验用户是否有表的操作权限。
词法分析 比如:这条简单的SQL语句,会被分割成10个字符串
csharp
# 分隔前
select c_id,first_name,last_name from customer where c_id=14;
# 分隔后
select,c_id,first_name,last_name,from,customer,where,c_id,=,14
MySQL 同时需要识别出这个SQL语句中的字符串分别是什么,代表什么。
- 把"select"这个关键字识别出来,这是一个查询语句
- 把"customer"识别成"表名 customer"
- 把"c_id识别成"列 c_id"
语法分析
如果语法正确就会根据 MySQL 语法规则与 SQL 语句生成一个数据结构,这个数据结构我们把它叫做解析树。
解析数举例:
预处理器
预处理器则会进一步去检查解析树是否合法,比如表名是否存在,语句中表的列是否存在等等,在这一 步MySQL会检验用户是否有表的操作权限。
预处理之后会得到一个新的解析树,然后调用对应执行模块。
5.4 第四步:优化SQL语句
优化器顾名思义就是对查询进行优化。作用是根据解析树生成不同的执行计划,然后选择最优的执行计划。
MySQL 里面使用的是基于成本模型的优化器,哪种执行计划Explain执行时成本最小就用哪种。而且它 是io_cost和cpu_cost的开销总和,它通常也是我们评价一个查询的执行效率的一个常用指标。 查看上次查询成本开销,默认值是0。
sql
show status like 'Last_query_cost';
化器可以做哪些优化呢?
- 当有多个索引可用的时候,决定使用哪个索引。
- 在一个语句有多表关联(join)的时候,决定各个表的连接顺序,以哪个表为基准表。
使用explain工具可以查看优化器的执行计划,后面SQL优化篇单独展开。
5.5 第五步:执行SQL语句
权限判断
开始执行的时候,要先判断一下你对这个表有没有执行查询的权限,如果没有,就会返回没有 权限的错误。
调用存储引擎接口查询
如果有权限,就使用指定的存储引擎打开表开始查询。执行器会根据表的引擎定义,去使用这个引擎提 供的查询接口提取数据。
-
c_id是主键执行流程:
- 调用 InnoDB 引擎接口,从主键索引中检索c_id=14的记录。
- 主键索引等值查询只会查询出一条记录,直接将该记录返回客户端。
- 至此,这个语句就执行完成了。
-
c_id不是主键执行流程:
- 全表扫描 调用 InnoDB 引擎接口取这个表的第一行,判断c_id 值是不是 14,如果不是则跳过,如果是 则将这行缓存在结果集中。
- 调用引擎接口取"下一行",重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的结果集返回给客户端。
- 至此,这个语句就执行完成了。
6、MySQL存储引擎
6.1 存储引擎分类
存储引擎 | 说明 |
---|---|
MyISAM | 高速引擎,拥有较高的插入,查询速度,但不支持事务 |
InnoDB | 5.5版本后MySQL的默认数据库存储引擎,支持事务和行级锁,比MyISAM处理 速度稍慢 |
ISAM | MyISAM的前身,MySQL5.0以后不再默认安装 |
MRG_MyISAM | 将多个表联合成一个表使用,在超大规模数据存储时很有用 |
Memory | 内存存储引擎,拥有极高的插入,更新和查询效率。 但是会占用和数据量成正比的内存空间。只在内存上保存数据,意味着数据可 能会丢失 |
Archive | 将数据压缩后进行存储,非常适合存储大量的独立的,作为历史记录的数据, 但是只能进行插入和查询操作 |
CSV | CSV 存储引擎是基于 CSV 格式文件存储数据(应用于跨平台的数据交换) |
引擎怎么选择?
除非需要用到某些InnoDB不具备的特性,并且没有其他办法可以替代,否则都应该选 择InnoDB引擎。也就是说,大部分情况下都选择InnoDB。
InnoDB和MyISAM存储引擎对比:
比较项 | InnoDB | MyISAM |
---|---|---|
存储文件 | .frm 表定义文件, .ibd 数据文件和索引文件 | .frm 表定义文件 .myd 数据文件 .myi 索引文件 |
锁 | 表锁、行锁 | 表锁 |
事务 | 支持 | 不支持 |
适应场景 | CRUD均可 | 读多 |
索引 | B+Tree | B+Tree |
6.2 InnoDB整体架构
上图详细展示了InnoDB存储引擎的体系架构,从图中可见,InnoDB存储引擎由内存结构、磁盘结构两 部分组成。
6.3 InnoDB内存结构
InnoDB 内存结构主要分为如下四个区域:
- Buffer Pool 缓冲池
- Change Buffer 修改缓冲池
- Adaptive Hash Index 自适应索引
- Log Buffer 日志缓冲
Buffer Pool 缓冲池
缓冲池Buffer Pool用于加速数据的访问和修改,通过将热点数据缓存在内存的方法,最大限度地减少磁盘IO,加速热点数据读写。
- 默认大小为128M,Buffer Pool中数据以页(16K)为存储单位,其实现的数据结构是以页为单位的单链表。
- 由于内存的空间限制,Buffer Pool 仅能容纳最热点的数据。
- Buffer Pool 使用LRU算法(Least Recently Used 最近最少使用)淘汰非热点数据页。
- LRU:根据页数据的历史访问来淘汰数据,如果数据最近被访问过,那么将来被访问的几率也更高,优先淘汰最近没有被访问到的数据。
- 对于Buffer Pool中数据的查询,InnoDB 直接读取返回。
- 对于 Buffer Pool中数据的修改,InnoDB直接在 Buffer Pool中修改,并将修改写入log buffer(redo log)。
Change Buffer 修改缓冲池
Change Buffer 用于加速非热点数据中二级索引的写入操作。由于二级索引数据的不连续性,导致修改二级索引时需要进行频繁的磁盘 IO 消耗大量性能,Change Buffer 缓冲对二级索引的修改操作,同 时将写操作录入 redo log 中,在缓冲到一定量或系统较空闲时进行 merge 操作将修改写入磁盘中。 Change Buffer 在系统表空间中有相应的持久化区域。
Change Buffer大小默认占 Buffer Pool的25%,最大50%,在引擎启动时便初始化完成。其物理结构 为一棵名为 ibuf的 BTree。
- ChangeBuffer用于存储SQL变更操作;
- ChangeBuffer中的每个变更操作都有其对应的数据页,并且该数据页未加载到缓存中;
- 当ChangeBuffer中变更操作对应的数据页加载到缓存中后,InnoDB会把变更操作Merge到数据页上;
- InnoDB会定期加载Change Buffer中操作对应的数据页到缓存中,并Merge变更操作
Adaptive Hash Index 自适应索引
自适应哈希索引(Adaptive Hash Index,AHI)用于实现对于热数据页的一次查询。是建立在索引之上 的索引!使用聚簇索引进行数据页定位的时候需要根据索引树的高度从根节点走到叶子节点,通常需要 3 到 4 次查询才能定位到数据。InnoDB 根据对索引使用情况的分析和索引字段的分析,通过自调优 Self-tuning的方式为索引页建立或者删除哈希索引。
AHI 的大小为 Buffer Pool 的 1/64,在 MySQL 5.7 之后支持分区,以减少对于全局 AHI 锁的竞争,默认 分区数为 8。
AHI 所作用的目标是频繁查询的数据页和索引页,而由于数据页是聚簇索引的一部分,因此 AHI 是建立 在索引之上的索引,对于二级索引,若命中 AHI,则将直接从 AHI 获取二级索引页的记录指针,再根据 主键沿着聚簇索引查找数据;若聚簇索引查询同样命中 AHI,则直接返回目标数据页的记录指针,此时 就可以根据记录指针直接定位数据页。
ini
# 查看innodb存储引擎状态,包含自适应哈希状态信息
show engine innodb status;
# 查看是否开启自适应哈希配置,默认是开启的
show variables like 'innodb_adaptive_hash_index';
Log Buffer 日志缓冲
InnoDB 使用 Log Buffer 来缓冲日志文件的写入操作。内存写入加上日志文件顺序写的特点,使得 InnoDB 日志写入性能极高。
对于任何修改操作,都将录入诸如 redo log 与 undo log 这样的日志文件中,因此日志文件的写入操作非常频繁,却又十分零散。这些文件都存储在磁盘中,因此日志记录将引发大量的磁盘 IO。Log Buffer 将分散的写入操作放在内存中,通过定期批量写入磁盘的方式提高日志写入效率和减少磁盘 IO。
注意:这种将分散操作改为批量操作的优化方式将增加数据丢失的风险!
6.4 InnoDB磁盘结构
系统表空间
系统表空间是 InnoDB 数据字典、双写缓冲、修改缓冲和回滚日志的存储位置,如果关闭独立表空间, 它将存储所有表数据和索引。
它默认下是一个初始大小 12MB、名为 ibdata1 的文件,系统表空间所对应的文件由 innodb_data_file_path 定义。
指定系统表空间文件自动增长后,其增长大小由 innodb_autoextend_increment
设置(默认为 64MB)且不可缩减,即使删除系统表空间中存储的表和索引,此过程释放的空间仅仅是在表空间文件 中标记为已释放而已,并不会缩减其在磁盘中的大小。
- 数据字典(Data Dictionary): 数据字典是由各种表对象的元数据信息(表结构,索引,列信息 等)组成的内部表
- 双写缓冲(Doublewrite Buffer):双写缓冲用于保证写入磁盘时页数据的完整性,防止发生部分写失效问题。
- 修改缓冲(Change Buffer): 内存中 Change Buffer 对应的持久化区域
- 回滚日志(Undo Log):实现事务进行 回滚 操作时对数据的恢复。是实现多版本并发控制 (MVCC)重要组成。
独立表空间
独立表空间用于存放每个表的数据和索引
。其他类型的信息,如:回滚日志、双写缓冲区、系统事务信息、修改缓冲等仍存放于系统表空间内。因此即使用了独立表空间,系统表空间也会不断增长。在5.7版 本中默认开启。
开启独立表空间(innodb_file_per_table=ON
)之后,InnoDB 会为 每个数据库单独创建子文件夹,数据库文件夹内为每个数据表单独建立一个表空间文件 table.ibd 。 同时创建一个 table.frm 文件用于保存表结构信息。
每个独立表空间的初始大小是 96KB。
通用表空间
通用表空间(General Tablespace)是一个由 CREATE TABLESPACE 命令创建的共享表空间,创建时必 须指定该表空间名称和 ibd 文件位置,ibd 文件可以放置于任何 MySQL 有权限的地方。该表空间内可以 容纳多张数据表,同时在创建时可以指定该表空间所使用的默认引擎。
通用表空间存在的目的是为了在系统表空间与独立表空间之间作出平衡。系统表空间与独立表空间中的 表可以向通用表空间移动,反之亦可,但系统表空间中的表无法直接与独立表空间中的表相互转化。
Undo 表空间
Undo TableSpace 用于存放一个或多个 undo log 文件。默认 undo log 存储在系统表空间中,MySql 5.7中支持自定义 Undo log 表空间并存储所有 undo log。一旦用户定义了 Undo Tablespace,则系统 表空间中的 Undo log 区域将失效。对于 Undo Tablespace 的启用必须在 MySQL 初始化前设置, Undo Tablespace 默认大小为 10MB。Undo Tablespace 中的 Undo log 表可以进行 truncate 操作。
临时表空间
MySQL 5.7 之前临时表存储在系统表空间中,这样会导致 ibdata 在使用临时表的场景下疯狂增长。5.7 版本之后 InnoDB 引擎从系统表空间中抽离出临时表空间(Temporary Tablespace),用于独立保存 临时表数据及其回滚信息。该表空间文件路径由 innodb_temp_data_file_path 指定,但必须继承 innodb_data_home_dir 。
6.5 磁盘文件之存储结构
段【Segment】
表空间由各个段(Segment)组成,创建的段类型分为数据段、索引段、回滚段等。由于 InnoDB 采用 聚簇索引与 B+ 树的结构存储数据,所以事实上数据页和二级索引页仅仅只是 B+ 树的叶子节点,因此数 据段称为 Leaf node segment,索引段其实指的是 B+ 树的非叶子节点,称为 Non-Leaf node segment。一个段会包含多个区,至少会有一个区,段扩展的最小单位是区。
- 数据段称为 Leaf node segment
- 索引段称为 Non-Leaf node segment
区(Extend)
区(Extend)是由连续的页组成的空间,大小固定为 1MB,由于默认页大小为 16K,因此一个区默认 存储 64 个连续的页。如果页大小调整为 4K,则 256 个连续页组成一个区。为了保证页的连续性, InnoDB 存储引擎会一次从磁盘申请 4 ~ 5 个区
页【Page】
页(Page)是 InnoDB 的基本存储单位,每个页大小默认为 16K,从 InnoDB1.2.x 版本开始,可通过 设置 innodb_page_size 修改为 4K、8K、16K。InnoDB 首次加载后便无法更改。
Linux的页一般是4K,通过命令getconf PAGE_SIZE
查看。
由此可知,InnoDB从磁盘中读取一个数据页,操作系统会分4次从磁盘文件中读取数据到内存。写入也 是一样的,需要分4次从内存写入到磁盘中。
行【Row】
InnoDB的数据是以行为单位存储的,1个页中包含多个行。在MySQL5.7中,InnoDB提供了4种行格 式:Compact、Redundant、Dynamic和Compressed行格式,Dynamic为MySQL5.7默认的行格式。
创建表可以指定行格式:
sql
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;
6.6 InnoDB内存数据落盘
在数据库中进行读取操作,将从磁盘中读到的页放在缓冲区中,下次再读相同的页时,首先判断该页是 否在缓冲区中。若在缓冲区中,称该页在缓冲区中被命中,直接读取该页。否则读取磁盘上的页。
对于数据库中页的修改操作,则首先修改在缓冲区中的页,然后再以一定的频率刷新到磁盘上。页从缓 冲区刷新回磁盘的操作并不是在每次页发生更新时都触发,而是通过一种称为CheckPoint的机制刷新回磁盘。
内存数据落盘要考虑的核心问题:高性能写入数据,同 时保证数据的绝对安全性!
写入性能如何保证?
分散写入操作放在内存中,通过定期批量写入磁盘的方式提高写入效率减少磁盘 IO。
如何持久化?
也就是修改后的数据如何到磁盘中去。内存里缓冲池中的数据页要完成持久化通过两个 流程来完成:
- 通过CheckPoint机制进行脏页落盘
- 日志先行,所有操作前先写Redo日志
数据安全性怎么保证?
- 记录操作日志:Force Log at Commit机制与Write Ahead Log(WAL)策略
- CheckPoint机制
- Double Write机制
什么是脏页?
对于数据库中页的修改操作,则首先修改在缓冲区中的页,缓冲区中的页与磁盘中的页数据不一致,所 以称缓冲区中的页为脏页。然后再以一定的频率将脏页刷新到磁盘上。页从缓冲区刷新回磁盘的操作并 不是在每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘。
为什么不是每次更新直接写入磁盘呢?
- 如果每次一个页发生变化就进行落盘,每次落盘一个页,必然伴随着4次IO操作,那么性能开销会 非常大。而且这个开销是随着写入操作的增加指数级增长的!
- 如果数据长期在内存中保存,那么数据就存在安全性风险!
- InnoDB采用了Write Ahead Log(WAL)策略和Force Log at Commit机制实现事务级别下数据的 持久性。
- Force Log at Commit机制:当事务提交时,所有事务产生的日志都必须刷到磁盘。如果日 志刷新成功后,缓冲池中的数据刷新到磁盘前数据库发生了宕机,那么重启时,数据库可以从 日志中恢复数据。这样可以保证数据的安全性
- write Ahead Log(WAL)策略:要求数据的变更写入到磁盘前,首先必须将内存中的日志 写入到磁盘;InnoDB 的 WAL(Write Ahead Log)技术的产物就是 redo log,对于写操 作,永远都是日志先行,先写入 redo log 确保一致性之后,再对修改数据进行落盘。
- 说白了保证数据的持久性与安全性我们采用记录日志的方式,那么也就是说,日志安全了,数 据就安全了
怎么确保日志就能安全的写入系统呢?
为了确保每次日志都写入到redo日志文件,在每次将redo日志缓冲写入redo日志后,调用一次 fsync操作,将缓冲文件从文件系统缓存中真正写入磁盘。
这样做不就等同于数据直接写入磁盘吗?
- redo日志不会记录完整的一页数据,因为这样日志太大,它只会记录那次(sequence)如何操作 了(update,insert)哪页(page)的哪行(row)
- 日志是顺序写入,而数据是随机写入。顺序写入效率更高
- 日志也不是改一条写一条,而是采用redo 日志落盘策略来兼顾安全性与性能!
- 可以通过 innodb_flush_log_at_trx_commit 来控制redo日志刷新到磁盘的策略。
Redo日志落盘
Log Buffer写入磁盘的时机由参数 innodb_flush_log_at_trx_commit 控制,此属性控制每次事务提交时 InnoDB的行为。是InnoDB性能调优的一个基础参数,涉及InnoDB的写入性能和数据安全性。默认1, 表示事务提交后立即落盘。
innodb_flush_log_at_trx_commit设置参数可参考: support.huaweicloud.com/bestpractic...
checkpoint机制
什么是checkpoint机制?
Checkpoint要做的事情是将缓冲池中的脏页数据刷到磁盘上。CheckPoint决定了脏页落盘的时机、条件 及脏页的选择,不同的CheckPoint做法并不相同。
CheckPoint要解决什么问题?
- 脏页落盘:避免数据更改直接操作磁盘
- 缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之 前的页都已经刷新回磁盘。数据库只需对Checkpoint后的redo日志进行恢复,这样就大大缩短了 恢复的时间。
- Dirty Page too much:即脏页数量太多,导致强制进行Checkpoint。由参数 innodb_max_dirty_pages_pt 来控制,默认75(即75%)。当脏页数量占据75%缓冲池时,刷新 一部分脏页到磁盘。(由Page Cleaner Thread完成)。查看命令为:
sql
show variables like 'innodb_max_dirty_pages_pct'
Double Write双写
脏页落盘出现的问题:写失效
我们知道脏页会在某些场景下进行刷盘,将缓冲池内的脏页数据落地到磁盘。因为存储引擎缓冲池内的 数据页大小默认为16KB,而文件系统一页大小为4KB,所以在进行刷盘操作时,就有可能发生如下场景:
如图所示,数据库准备刷新脏页时,将16KB的刷入磁盘,但当写入了8KB时,就宕机了这种情况被称为 写失效(partial page write)。
怎么解决?
Doublewrite其实就是写两次,解决写失效问题,需要用到Doublewrite机制,简单来说就是在redo日志落盘前,对需要写入的页的做个副本,当写失效发生时,通过页的副本来还原该页再重做,这就是所谓的 double write。写失效后redo日志也是无法进行恢复的,因为redo日志记录的是对页的物理修改。
Double write脏页刷新流程:
- 首先复制:脏页刷新时不直接写磁盘,而是先将脏页复制到内存的Doublewrite buffer。
- 再顺序写:内存的Doublewrite buffer分两次,每次1MB顺序地写入共享表空间的物理磁盘上,会立即调用fsync函数同步OS缓存到磁盘中,顺序写性能好
- 最后离散写:内存的Doublewrite buffer最后将页写入各自表空间文件中,离散写较顺序写入差一 些。
Double write崩溃恢复
如果脏页数据未来得及落盘,系统就奔溃了,直接应用redo日志重新执行脏页落盘。
如果操作系统在将页写入磁盘的过程中发生了崩溃,其恢复过程如下:
- 首先InnoDB存储引擎从系统表空间中的Double write中找到该页的一个副本 。
- 然后将其复制到独立表空间。
- 最后清除redo日志,完成数据恢复。
7、总结与最佳实践
通过架构分析可见,MySQL的高性能源于:
- 日志先行(WAL)机制保证持久性。
- 缓冲池设计减少磁盘IO。
- 多版本并发控制提升并发能力。
生产环境建议:
- 监控Buffer Pool命中率
- 控制脏页比例(<75%)
- 合理设置innodb_flush_log_at_trx_commit
- 定期验证备份有效性
随着云原生时代到来,MySQL架构正在向计算存储分离、智能优化方向发展,但核心设计思想仍值得深入研究和借鉴。
参考文章:
02 | 日志系统:一条SQL更新语句是如何执行的?-MySQL 实战 45 讲-极客时间
zhuanlan.zhihu.com/p/641557580