两张图彻底搞懂 MySQL InnoDB 的数据页、索引页、redo log、binlog、undo的文件、内存、刷盘机制

公众号:爱码叔漫画软件设计(搜:爱码叔)

CSDN: https://blog.csdn.net/liyiming2017

知乎:https://www.zhihu.com/people/zhong-guo-ren-83-43

专注于软件设计与架构、技术管理。擅长用通俗易懂的语言讲解技术。对技术管理工作有自己的一定见解。欢迎大家关注我!

MySQL 数据库以及 InnoDB 引擎的数据存储由很多种类型的文件所构成。这些文件是存储数据、日志、事务信息的载体。在实现数据库的持久性、可靠性,以及数据库恢复、数据库复制等各个方面,都起到了重要的作用。

本文主要围绕表空间文件、redo log、binlog、undo log 这几种最重要的文件(日志),讲解 MySQL 核心功能的实现,涉及 MySQL 和 InnoDB 对磁盘、内存的使用,核心线程及其工作方式。全文主要围绕两张图进行说明,一张是 “文件、内存全景” 图,另外一张是 “工作过程” 图。

首先看下面第一张图, “文件、内存全景” 图。此图涵盖了 MySQL InnoDB 中使用的文件、内存、相关参数。

all.jpg

文件是数据、日志的最终落脚点。内存是数据、日志的临时栖息点。无论文件还是内存,所管理内容是一致的。下面我们分为文件和内存两部分,对这张图进行讲解。

一、文件

MySQL 由很多文件构成,目的各不相让相同。例如表空间文件主要用来存储数据、索引。日志文件用来记录 MySQL 运行过程中产生的日志。其中有些日志是 MySQL 实现某种功能所必需的。例如 redo log,MySQL InnoDB 引擎用 redo log 来保证数据的完整性。有些日志,例如慢查询日志,则是供数据库的使用人员分析问题。

下面表格总结了常用的一些文件。

文件 文件名 作用 级别
独立表空间文件 *.ibd 保存单表的数据、索引等信息。 InnoDB
共享表空间文件 ibdata1 主要是保存undo日志、事务、二次写缓冲等信息。 InnoDB
重做日志(redo log) ib_logfile0 ib_logfile1 保证数据的完整性。当数据库宕机,使用redo log恢复。 InnoDB
二进制日志(binlog) mysqlbin.000001 记录所有对MySQL数据库的更改。不包含select、show这类查看相关的操作。 MySQL
表结构定义文件 *.frm 每张表、视图对应一个文件。记录表结构信息。 MySQL
错误日志 hostname.log 记录MySQL启动、运行、关闭,以及遇到的错误、警告信息。DBA可以用来定位问题。 MySQL
慢查询日志 slow.log 记录超过设定阈值的慢查询。开发人员可以选择其中的慢查询进行优化。需要手动开启。 MySQL
查询日志 hostname.log 记录所有对MySQL数据库的请求。 MySQL

下图展示了MySQL对存储的使用划分,以及文件和存储内容的对应关系。

文件.jpg

下面我们重点看其中几种文件:表空间文件、重做日志文件(redo log)、二进制日志文件(bin log)、undo log。

1.1 表空间文件

表空间主要用来存储表中的数据以及索引。通过设置 Innodb_file_per_table 参数,可以开启一张表一个表空间文件。但共享表空间同样还是存在的,如上图所示,一些数据仍然需要存储在共享表空间。

独立表空间文件以 表名.ibd 命名,共享表空间文件名为ibdata1。共享表空间可以由多个文件构成,例如ibdata1、ibdata2。

我们来重点看看表空间的存储结构。表空间是用来管理数据的地方,逻辑上按照段(segment)、区(extent)、页(page)进行逐级管理。这就好比《四大名著》套装。表空间相当于这一套书的盒子。段相当于不同的书籍,例如Leaf node segment相当于《西游记》,Non-Leaf node segment相当于《水浒传》。区相当于每本书中的章。页就不用多说,应该都能理解。页中保存的便是一行行的数据。

层级关系是这样的,Segment->Extent->Page->Data Row->column。需要注意这是对表空间文件的逻辑划分,在物理上可能是一个文件,也可能是多个文件。每个表空间文件内部的管理、记录方式都是按照段、区、页进行。

我们在上图的基础上,补充表空间的逻辑划分。

表空间.jpg

1.1.1 段(Segment)

常见的段 包括数据段、索引段、undo 段等。我们知道 InnoDB 使用 B+ 树组织索引和数据。数据被保存在聚集索引 B+ 树的叶子结点中,那么 Leaf node segment 就是数据段。Non-Leaf node segment 是索引段。

1.1.2 区(Extent)

区是段的下一级逻辑单元。可以认为区是一个固定大小的,存放页的容器。一个段由多个区组成,每个区存放着固定数量的页。

区中的页是连续的,InnoDB 一次申请 4-5 个区。区的大小为固定 1MB。能存放页的数量,取决于设置的页大小。通过参数 innodb_page_size 可以修改每页大小,默认 16KB,也可以设为 8KB、4KB。对应每个区存储的页的数量为64、128、256。

1.1.3 页(Page)

InnoDB 对数据管理的最小单元为页,页的大小可以通过 innodb_page_size 参数设置,默认 16KB。设置后不可更改。页中存储的除了数据和索引的 B+ 树结点外,还有二机制大对象页,以及 undo 页,系统页等上面图中表空间所展示的存储内容。

1.1.4 表空间相关参数

参数名称 默认值 作用
innodb_file_per_table ON 每张表使用单独的表空间文件
innodb_page_size 16KB 设置每页大小

1.2 重做日志文件(redo log)

重做日志文件是 InnoDB 的 log 文件,因此在 InnoDB 中直接称其为 log,它对 InnoDB 的工作起着重要的作用。由于 InnoDB 对缓存的重度使用,可能会导致在数据库宕机时,内存中的数据还未被写入磁盘。这是一个相当严重的问题,破坏了数据的完整性和 ACID 中的持久性。重做日志便是用来解决此问题的关键一步。

1.2.1 重做日志的作用

重做日志中记录了 InnoDB 引擎的事务日志。换句话讲,重做日志记录了数据库的完整操作。当数据库宕机重启后,找到磁盘最后一次刷盘的记录点,然后在重做日志中找到这个记录点以后的日志,便可以将数据库磁盘中的数据恢复到宕机前内存中的状态。这个记录点在MySQL中叫做 Checkpoint,这是一个重要概念,关系到 InnoDB 何时写磁盘、如何恢复。不过在此不展开,后面详细阐述。

1.2.2 重做日志记录方式

重做日志至少存在一个文件组(group),如图1-2所示。组中至少有两个日志文件,循环记录。

单个重做日志文件的大小可以通过 innodb_log_file_size 进行指定,但是总的大小不能超过512GB。重做日志文件的大小会影响数据库的表现。太大的话,如果宕机时的 checkpoint 比较靠前,数据库恢复会比较慢。太小的话,一是会导致大的事务需要切换日志文件,二是导致 checkpoint 频繁发生影响性能。这是因为重做日志即将写满时,会导致 checkpoint 发生,数据库将数据从内存写到磁盘,同时将 checkpoint 前移,达到释放重做日志的目的。如果频繁发生 checkpoint,会严重影响数据库的性能。

重做日志在写入的时候,会先写入内存的缓冲区,然后再写入磁盘。一次写入磁盘512字节,一个扇区的大小,保证写入的成功。

InnoDB 的 Master Thread 每秒都会将缓存中的日志写入磁盘。但是并不能保证缓存池中数据的状态和重做日志一致。看这个场景,当事务提交后,缓存中的数据已经变更,但是重做日志还未写入磁盘,此时发生宕机,重做日志和数据状态便不再一致。数据库重启后无法使用重做日志将数据恢复到宕机前的状态,丧失了ACID中的持久性。因此,在事务提交的时候,需要强制写一次重做日志,我们只需要设置 innodb_flush_log_at_trx_commit=1.

1.2.3 重做日志相关参数

参数名称 作用
innodb_log_file_size 一个日志文件的大小
innodb_log_files_in_group 一个组中包含的日志文件数量
innodb_mirrored_log_groups 日志文件组镜像的数量
innodb_log_group_home_dir 日志文件组所在位置
innodb_flush_log_at_trx_commit 0:事务提交,日志不写磁盘;1:事务提交时日志同步写磁盘;2:事务提交时日志异步写磁盘;如果想获得DB的持久性,需要设置为1

1.3 二进制日志(binlog)

binlog是MySQL数据库级别的日志,记录了所有对MySQL数据库的更改。

1.3.1 二进制日志的作用

二进制日志比较常见的使用场景是主从同步。由于二进制文件记录了所有的数据库更改,因此可以用来恢复数据库、复制数据库。此外,日志中能看到所有的操作信息,还可以用来审计。

1.3.2 二进制日志记录方式

首先,二进制日志需要手动打开,通过参数log-bin=[name],指定文件名,否则为主机名。

binlog 在事务未提交前,记录在缓存中,当事务提交时,将缓存中的数据写入磁盘中的二进制日志文件。如果想要避免缓存中数据未能记录,可以通过设置参数 sync_binlog=1,不用缓存,同步写磁盘。当然这会影响性能,但是更加安全。

即使设置了同步写,仍然还可能出现问题。考虑如下场景。在事务提交时,binlog 已经写磁盘完成,此时数据库宕机,导致事务没有真正完成提交,因此重做日志没有落盘。重启时,数据库会回滚这次事务提交,重做日志记录的状态为数据提交前,而 binlog 记录的状态为事务提交后。由于数据库的最终状态由重做日志所决定,此时 binlog 已经和数据库状态不一致。

想要避免这个问题,需要开启 innodb 的内分布式事务 XA,通过设置 innodb_support_xa = 1。内部 XA 事务用来处理存储引擎和插件间、引擎和引擎间的事务问题。重做日志是 innoDB 级别,binlog 是 MySQL 级别,是内部XA 事务的典型场景。

开启 XA 后,在数据库事务提交时,会记录 XA 事务(区别于数据库事务)的 xid。一旦发生 binlog 写成功,重做日志写失败的场景,在数据库重启回复的时候,数据库会检查xid事务是否提交,如果没有提交,将会重新提交该xid 事务,确保了重做日志和 binlog 保持一致。在 Master/Slave 的场景下,binlog 和重做日志保持一致,也就确保了主从一致。

内部XA.jpg

1.3.3 二进制日志记录格式

binlog 有3种记录格式,通过 binlog_format 参数进行设置。三种格式为 STATEMENT、ROW、MIXED。

STATEMENT 记录逻辑SQL,优势是记录的文件会比较小。原因在于,一个全表的 update 操作,只需要记录一条语句,而 ROW 格式需要记录每一行的变化,这个对比非常悬殊。但是由于有些系统函数和自定义函数多次执行的结果并不一致,例如 UUID() 函数,每次执行产生的 ID 并不相同,这会导致回放 SQL 语句的时候,得到不一样的数据。因此 STATEMENT 格式的使用需要谨慎。

ROW 格式记录的是每一行的变化。记录的内容相比于 STATEMENT 格式会更多,但会更加可靠。

MIXED 从名字可以看出是以上两种格式的混合。优先使用更节省空间的 STATEMENT 格式记录,在 STATEMENT 格式可能会导致问题的场景,使用 ROW 格式记录。例如使用了某些系统函数和用户自定义的函数。一般情况下,推荐使用 MIXED 记录方式,它集成了两种格式优势。

1.3.4 二进制日志相关参数

可以看到 binlog 相关的参数并没有 innodb 的前缀,这是因为 binlog 是 MySQL 级别的日志。

参数名称 作用
log-bin 开启二进制日志,并指定文件名
binlog_cache_size 基于会话的binlog缓存大小
max_binlog_size 单个二进制日志文件的大小
sync_binlog 值为N,代表写N次缓存后落盘。设置为1代表同步写磁盘,不使用缓存。
log-slave-update 主从结构中的slave,不会记录从master同步过来的binlog,如需记录需要开启这个参数。
binlog_format 日志格式
innodb_support_xa 开启内部XA事务,确保binlog和重做日志一致

1.4 undo

undo 严格意义上讲并不算日志,它是 InnoDB 引擎记录的事务过程数据,是事务操作的必需品。一般存放在共享表空间中的 rollback segment中。当然也可以指定存放共享表空间之外的位置,通过设置innodb_undo_directory 参数。

1.4.1 undo 的作用

InnoDB 引擎使用 undo 记录事务的操作以及数据的版本,以便事务失败时进行回滚。此外,undo 还实现了数据的 MVCC。

1.4.2 undo 的记录方式

undo 的管理方式也是段、区、页。InnoDB 有 rollback segement,每个 rollback segement 中有 1024 个 undo log segment。

当事务进行提交时,意味着 undo log 已经没有太大的保留价值。InnoDB 会先将 undo log 记录到淘汰列表中。并不会直接删除。这是因为 undo log 还一个作用是记录数据的历史版本,此时可能有别的事务用到了这个 undo log 中的数据版本。InnoDB 中有一个 Purge 线程,会异步处理 undo log 的清理工作。

1.4.3 undo 分类

undo log 分为 insert 和 update 两种类型。

insert 类型的 undo log 记录 insert 操作。insert 操作不会修改已有数据,而是产生新的数据,其它事务本来就无法读取,所以不用因为 MVCC 的原因而保留。事务提交后可以直接删除。

update 类型的 undo log 记录 delete 和 update 操作。由于需要提供 MVCC 支持,不能在事务提交时删除。需要等待Purge线程的处理。

1.4.3 undo 相关参数

参数名称 作用
innodb_undo_directory 设置 undo log 存储的位置,undo log 可以独立于共享表空间存储。
innodb_undo_logs 设置 rollback segment 的个数,默认128
innodb_undo_tablespaces undo log 的表空间数量,即存储 undo log 的文件数量。

二、内存

上文中,我们分析了 MySQL InnoDB 引擎工作时用到的主要文件。这些文件中的数据,并不是直接读取或者写入磁盘的。出于性能的考虑,MySQL InnoDB 大量使用内存作为缓冲。

以表空间文件为例,存储数据的最小容器单位为页,InnoDB 会将页放入内存中的缓冲池中,下一次读取该页的时候,首先从缓冲池中获取。InnoDB 对页的修改,也是先修改缓冲池中该页。再以某种频率和机制写到磁盘上。这个机制就是之前提到的 Checkpoint 机制。

下面是 InnoDB 的内存使用示意图。

2.1 缓冲池

缓冲池中缓存的主要数据是数据页和索引页,这也是最重要的数据。缓冲池可以通过 innodb_buffer_pool_size 设置大小。通过 innodb_buffer_pool_instances 可以设置缓存池的数量,提升数据库的性能。

2.1.1 缓冲池中页面的刷盘机制

缓冲池中存放大量的页,需要进行有效的管理,才能确保保留在缓存中的是比较热的数据。这里使用的是我们常见的 LRU 算法,即最近最少使用算法。当缓存池满时,按最近最少使用来释放缓存的页。

InnoDB 维护了一个 LRU 的列表,列表中维护了页的 LRU 顺序。InnoDB 对 LRU 算法做了改进,改进点有如下三个。

  1. LRU 列表划分为两个部分,前5/8为 new 区域,后3/8为 old 区域。这个比例可以通过 innodb_old_blocks_pct参数配置。

  2. 当有新的页进入缓存时,先插入在 old 区的队首。

  3. 在一段时间后,如果对该页面还有访问,才会将它移到 LRU 队首。

这样做的原因是,某些 SQL 操作可能会涉及到大量页的读取,但只和本次操作有关。如果大量页直接进入 LRU 队首,可能会把真正活跃的页顶出LRU列表。改进后,大量页第一次进入缓冲池时,只会顶出 LRU 列表中的后 3/8 的页,保留了前面 5/8 的页。

随后,这些页可能因为本次 SQL 操作还会被访问到,但 InnoDB 还是不想让这些页只是因为一次 SQL 操作的需要就进入new区域。所以会根据配置(innodb_old_blocks_time),等待一段时间后,再去监控这些页的使用情况,如果这些页再次被访问,才会让其进入new区域。

总结一下,InnoDB 会让新缓存的页先进入 old 区,在 old 区停留一段时间后,才会让其根据 LRU 算法进入 new区,避免新页污染 LRU 列表。

LRU.jpg

2.1.2 缓存池相关参数

参数名称 作用
innodb_buffer_pool_size 设置缓冲池的大小
innodb_buffer_pool_instances 设置缓冲池的数量
innodb_old_blocks_pct 设置LRU列表中old区的比例大小,默认37
innodb_old_blocks_time 设置新的缓存页面首次被放入old区多久后,才有资格进入new区

2.2 重做日志缓冲

重做日志也在内存中存在缓冲区,并且独立于缓冲池,可以单独设置大小(innodb_log_buffer_size)。

2.2.1 重做日志缓冲刷盘机制

重做日志每秒都会被写入磁盘,因此不需要将缓冲区设置的过大,只需要不小于每秒产生的日志量即可。

重做日志从内存写到磁盘的时机如下。

  1. Master Thread 每秒写入磁盘
  2. 事务提交时,写入磁盘。需要通过设置 innodb_flush_log_at_trx_commit=1 开启
  3. 当重做日志缓冲池剩余空间小于 1/2 时,重做日志写入磁盘

2.2.1 重做日志缓冲相关参数

参数名称 作用
innodb_log_buffer_size 设置重做日志缓冲的大小
innodb_flush_log_at_trx_commit 设置事务提交时,如何刷盘。1:强制刷盘

2.3 额外内存池

每个缓冲池中的帧缓冲以及对应的缓冲控制对象,记录了一些 LRU、锁、等待等信息,需要从额外内存池申请。因此,当申请了很大的缓冲池时,额外内存池也要相应的扩大。

三、InnoDB 的核心线程及其工作方式

InnoDB为 单进程,多线程架构。主要有 Master 线程、IO Thread、Purge Thread、Page Cleaner Thread。

3.1 InnoDB 主要线程介绍

Master Thread 在早期的版本中承担很多工作,但随着清理 undo 页的工作交给了 Purge Thread,刷新脏页(修改发生在缓存,还未写入磁盘的页)交给了 Page Cleaner Thread,Master Thread工作已经减轻很多, 比较核心的工作主要是重做日志的刷盘和合并插入缓存。

InnoDB 使用了大量 AIO 处理 IO 请求,IO Thread 负责这些请求的回调处理,分别有 insert buffer thread、log thread、write thread、read thread。其中 write 和 read thread 可以通过参数设置数量(innodb_read_io_threads 和 innodb_write_id_threads)。

这些线程相互协调配合,完成 InnoDB 的工作。在 ”文件“ 一节中,已经介绍了 MySQL InnodDB 记录重做日志、二进制日志、undo 的工作方式。但是,还有一个最重要的组成部分——数据、索引页的更新机制没有说到。相比较而言,数据和索引页的更新机制更为复杂,下面我们展开讲解。

3.2 数据/索引页刷盘机制

数据和索引页的更新首先发生在缓冲池中,然后通过 Checkpoint 机制,从内存写入磁盘。前文也提到过Checkpoint 机制,简单来说这是 InnoDB 刷盘,并记录最后一次刷盘时,每个数据页所处于的重做日志位置的机制。

3.2.1 什么是 Checkpoint

对于 InnoDB 来说,事务提交后,数据只需要被写入缓冲池中的页,重做日志写入磁盘,就可以保证数据的持久性。DB 重启时,可以根据重做日志将数据恢复到磁盘上。当然这是理想情况,需要有很大的磁盘空间记录重做日志,而且内存也要足够大,能容纳所有的数据。即使满足了以上两个条件,每次重启时,也需要很长的时间才能通过重做日志同步磁盘数据。因此,InnoDB 在运行过程中,需要持续地将内存中发生变化的脏页,保存到磁盘中。

InnoDB 采用 Checkpoint 机制将脏页按一定频率和触发机制刷入磁盘,带来如下好处。

  1. 清理缓冲池中的页时,可能遇到脏页。刷入磁盘后,再清理。
  2. 重做日志大小可控。当重做日志即将超出设定的大小时,可以把未刷盘的那部分日志对应的脏页刷盘,从而释放掉这部分重做日志(不再需要这些日志做宕机恢复)。
  3. 因为缓冲池和磁盘中的数据差距不会太大,缩短了意外宕机后,重启的恢复时间。

数据库当前的数据版本,通过 LSN(Log Sequence Number)来标记。磁盘上每个页都有自己记录的LSN,从重做日志可以获取当前最新的LSN,代表数据库的最新数据版本。两者之间的差距,便是没有写入磁盘的数据范围。

当Checkpoint发生时,将会刷新脏页到磁盘,同时记录 Checkpoint 发生时的 LSN。那么,等到宕机后重启时,只需要恢复 Checkpoint 发生时记录的 LSN 到重做日志 LSN 之间的这些重做日志内容即可。例如 InnoDB 在恢复时,发现某个页的 LSN 是 12000,但是重做日志的 LSN 是 13000,那么该页需要通过重做日志恢复 12000-13000 这部分重做日志的变化。

3.2.2 Checkpoint 产生的时机

InnoDB 会选择适当的时机将缓冲池中的脏页写入磁盘,这也是产生 Checkpoint 的时机。Checkpoint 有如下两类。

  1. Sharp Checkpoint。这种 Checkpoint 发生时,会将一份重做日志中关联的所有脏页全部写入磁盘。
  2. Fuzzy Checkpoint。这种 Checkpoint 发生时,只会刷新一部分脏页。

这里有两种主要存在以下刷盘的时机点。

  1. 缓冲池的空闲页少于 100个,需要移除 LRU 尾端页,如果含有脏页,需要行刷盘。属于 Fuzzy CheckPoint
  2. 重做日志文件即将写满时触发刷盘。以达到更新重做日志 LSN,释放掉不需要的日志。InnoDB 会比较重做日志的 LSN 和设置的 async_water_mark、sync_water_mark,决定触发异步刷新或者同步刷新。属于 Sharp Checkpoint。
  3. 当缓存池中脏页比例超过 innodb_max_dirty_pages_pct 设定的值时(默认75),进行刷盘。属于 Fuzzy CheckPoint。
  4. Master Thread/Page Cleaner Thread 每秒、每十秒进行异步刷新。(可能发生)。属于 Fuzzy CheckPoint。
  5. 数据库正常关闭时,将所有脏页写入磁盘。属于 Sharp Checkpoint。

在这几种刷盘时机中,我们重点看看 Master Thread/Page Cleaner Thread 的定时刷新。在 InnoDB 1.2.x 之前,这个工作由Master Thread承担,之后由 Page Cleaner Thread 承担。为了简化描述,后面用 Master Thread 讲解。

  1. Master Thread 每次刷盘的脏页数量限制

由于 Master Thread 的处理能力有限制,所以每秒的刷盘操作也有页的数量限制。最初该限制为一次刷盘不超过100个页。但是随着 SSD 的使用,磁盘的 IO 能力大幅上升,所以 InnoDB 提供了 innodb_io_capacity参数,可以根据服务器磁盘的能力进行设置,默认值为 200。这个值也是 Master Thread 每次刷新页的数量上限。如果发现磁盘的 IO 能力还有富裕,可以上调该值

  1. Master Thread 刷盘前对脏页比例的判断

Master Thread 触发每秒/十秒刷盘时,也会先判断缓冲池中的脏页比例是否超过了设定的阈值,超过才刷新。设置参数为 innodb_max_dirty_pages_pct,默认75,也就是说脏页比例超过 75% 才会刷盘。

刷盘时机的第3种情况中,也用到了 innodb_max_dirty_pages_pct,达到该阈值,即使没有触发每秒/十秒刷盘,也会进行刷盘。这个值设置的过低,会导致刷盘过于频繁,磁盘 IO 承担过大压力。设置的过高会导致刷盘频率降低,一次刷盘的压力过大,此外如果内存很大,宕机恢复需要更长的时间。

  1. 自适应刷新脏页数量

由于发生 Sharp Checkpoint时,将会突破 innodb_io_capacity 设置的一次刷新脏页数量限制,这可能导致 IO 的压力上升。

例如重做日志快要写满时,发生 Sharp Checkpoint,其中一份重做日志关联的所有脏页都将被写入磁盘,无论脏页比例是否达到 innodb_max_dirty_pages_pct。如果脏页的数量非常多,那么 IO 压力上升,将会影响正常的读写操作。

InnoDB 提供一种监控重做日志增长速度以及脏页的数量,来决定每秒刷盘脏页数量的机制。将 IO 压力平均到每秒的刷盘操作中,避免释放重做日志时,一次刷新大量的脏页。这个特性 InnodDB 默认开启,也可以修改 innodb_adaptive_flushing=OFF 关闭。通过设置参数 innodb_adaptive_flushing_lwm ,可以控制重做日志达到该使用比例后,自动开启自适应刷盘。

3.2.3 两次写(doublewrite)

InnoDB 将缓冲池中的数据写入磁盘时,可能出现在写入中途失败的情况。这会导致表空间中的页被破坏。由于重做日志是物理操作日志,所以此时无法使用重做日志恢复。这是十分严重的可靠性问题。

InnoDB 使用两次写(doublewrite)的方案解决这个问题。此方案每次写入磁盘 2MB 的数据(2个区,128个页大小)。首先将2MB的脏页写入内存中的 doublewrite buffer(通过memcpy函数复制)。再从 doublewrite buffer 分两次,每次 1MB 写入位于共享表空间的 doublewrite 区,马上调用fsync函数同步磁盘。将2MB的脏页暂存在 doublewrite 区后,再从 doublewrite buffer 写入目标表空间文件。

两次写方案如下图所示。

doublewrite.jpg

这个方案解决了如下问题。

  1. 写入 doublewrite 区的中途如果失败,不会破坏目标表空间文件中的页。因此不影响重做日志的恢复工作。
  2. 写入目标表空间文件的中途如果失败,在数据库恢复的过程中,可以找到 doublewrite 区的该页副本,修复被破坏的页,然后用重复日志恢复。

3.2.4 临近页刷新

InnoDB提供刷新临近页的机制,在刷新某个脏页时会检查同区的所有页,将所有脏页一并刷新。这样做可以合并IO操作,一次刷新多个脏页。如果磁盘的IO性能不高,建议开启该特性。但是可能引入新的问题,同区的脏页可能处于LRU的队首,意味着刷盘后很可能再次变成脏页,此时并不是一个好的刷新时机。因此,如果磁盘的IO性能比较好,建议关闭该特性。

3.4 一次完整事务提交后,数据如何被写入磁盘

我们了解 MySQL InnoDB 最为核心的几项工作:保存数据、保存重做日志、保存 binlog、保存 undo。这些工作由不同的线程异步完成,如何配合是值得思考的问题。下面我们通过拆解一次数据库事务从开始到提交的过程,从整体视角来分析 InnoDB 的工作方式。

我们从下图入手,逐步分析。这也是我标题中所说的第二张

update.jpg

3.4.1 事务处理相关线程的工作

  1. 事务提交前

开始事务后,事务的处理线程会执行 SQL 操作.操作前先记录 undo log,然后修改数据,在这个过程中 redo log也在同时被记录。这里注意写 undo log时,也会记录redo log,也就是说用 redo log 恢复 DB 时,也会恢复 undo 页。我查询了很多资料,没有找到队 undo log 如何刷盘的描述。鉴于 undo 也需要 redo 进行恢复,我推测 undo log 页和数据页、索引页一样,采用了类似的刷盘机制。这个观点仅供参考。binlog 根据参数配置,选择是立即刷盘还是暂时放到缓冲中。

可以看到事务处理过程中,操作基本都发生在内存中,这是为了获取更高的性能。

  1. 事务提交后

事务提交后首先刷盘 binlog,然后是 redo log 。数据页可以通过 redo log恢复。这意味着 redo log 写入磁盘后,已经完成了数据的持久化。这是一个标志性事件。

此时,与此事务相关的 undo log 已经没有存在的意义,因为事务不需要被回滚了。相关的 undo log被放入内存中的 undo log 淘汰列表。等待 Purge 线程完成最终清理。

3.4.2 Master 线程的工作

早期的 Master 线程工作很多,包括脏页刷盘、redo log 刷盘、清理 undo log 等工作。InnoDB 在后续的版本中,将脏页刷盘交给了单独的 Page Cleaner 线程。undo log 清理工作交给了Purge线程。Master线程主要工作还有redo log 的定时刷盘。如图中红色底色的箭头标注,负责将redo log从缓冲写入磁盘文件。

3.4.3 Page Cleaner 线程的工作

Page Cleaner 线程负责脏页刷盘的工作。记录在内存中的数据页更新,最终由 Page Cleaner 线程写入磁盘。这个过程前文已经详细描述,此处不再赘述。

3.4.4 Purge 线程的工作

Purge 线程负责清理 undo log。InnodDB 在处理数据时并没有真的将原始数据处理掉。例如 delete 操作,只是先把记录标记为deleted。这是因为这条数据可能正在被其他事务所使用。等到 Puge 线程清理 undo log 时,其它事务已经不再使用这条记录,所以Puge线程此时会将数据彻底从B+树上物理删除掉。于此同时,释放掉相关的 undo log。

3.5 总结

简单来说,InnoDB 的工作方式为主流程尽可能使用缓存进行操作,各个专职的线程异步将缓存写入磁盘。事务提交是一个关键的时间点,想要保证数据库的持久性和可靠性,此时必须将 redo log写入磁盘。如果开启了binlog,尤其是主从架构的主机,此时也必须刷盘 binlog,并且设置innodb_support_xa = 1。这样可以确保 redo log 落后 binlog 时,可以重新提交xa事务,恢复redo log。否则,一旦 redolog 和 binlog 不一致,就会造成主从不一致。 InnoDB 涉及的知识非常多,本文不可能全部涉及,有些比较重要的内容并没有涵盖进来,比如插入缓存、日志格式、数据页记录格式等等。日后有时间我再总结成文。此外还有很重要的一块内容,事务、锁相关的内容没有涵盖,我也会写新的文章进行总结。

All rights reserved
Except where otherwise noted, content on this page is copyrighted.