MySQL 篇-深入了解 InnoDB 引擎的逻辑存储结构、架构、事务原理、MVCC 原理分析(RC 级别、RR 级别)

1.0 InnoDB 引擎 - 逻辑存储结构

逻辑存储结构如图所示:

1)表空间(ibd 文件)

在 MySQL 中,一个实例能够对应多个表空间。表空间作为数据存储的重要载体,用于存放记录、索引以及表结构等各类数据。

2)段

段可细分为数据段、索引段和回滚段。由于 InnoDB 采用索引组织表的方式,数据段实际上就是 B+ 树的叶子节点,而索引段则是 B+ 树的非叶子节点。段的主要作用是对多个区进行统一管理。

3)区

区是表空间的基本单元结构,每个区的大小固定为 1MB。默认情况下,InnoDB 存储引擎的页大小为 16KB,这意味着一个区中包含 64 个连续的页。

4)页

页是 InnoDB 存储引擎进行磁盘管理的最小单位,其默认大小为 16KB。为确保页的连续性,InnoDB 存储引擎在从磁盘申请空间时,每次会申请 4 - 5 个区。

5)行

InnoDB 存储引擎的数据是以行的形式进行存储的,每一行都包含了特定的数据信息。

6)Trx_id

每当对某条记录进行修改操作时,系统会将当前事务的 ID 赋值给名为 trx_id 的隐藏列,以此来记录该记录的修改事务信息。

7)Roll_pointer

每次对某条记录进行修改时,系统会将该记录的旧版本信息写入到 undo 日志中。而 Roll_pointer 这个隐藏列就如同一个指针,通过它可以找到该记录在修改之前的详细信息。

2.0 InnoDB 引擎 - 架构

MySQL 5.5 版本开始,默认使用 InnoDB 存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。

下面时 InnoDB 架构图,左侧为内存结构,右侧为磁盘结构:

2.1 InnoDB 引擎 - 内存结构

1)Buffer Pool(缓冲池)

缓冲池是主内存中的一块区域,它能够缓存磁盘上那些被频繁操作的真实数据。在执行增删改查操作时,系统会优先操作缓冲池中的数据。若缓冲池中不存在目标数据,系统会从磁盘加载该数据并将其缓存到缓冲池,随后按照一定的频率将缓冲池中的数据刷新到磁盘。这种机制有效减少了磁盘 I/O 操作,显著加快了数据处理速度。

缓冲池以 Page(页)为基本单位进行数据管理,其底层采用链表这种数据结构来组织和管理各个 Page。根据 Page 的状态,可将其分为以下三种类型:

  • free page(空闲页):尚未被使用的 Page,处于闲置状态。
  • clean page(干净页):已被使用的 Page,但其中的数据未被修改过,与磁盘上的数据保持一致。
  • dirty page(脏页):同样是已被使用的 Page,但数据已被修改,导致页中的数据与磁盘数据不一致。为保证数据的一致性,缓冲池会按照一定频率将脏页中的数据刷新到磁盘。

2)Change Buffer(更改缓冲区)

更改缓冲区主要针对非唯一的二级索引页。当执行 MDL 语句时,如果相关的数据 Page 不在 Buffer Pool 中,系统不会直接对磁盘进行操作,而是将数据变更信息存放到更改缓冲区 Change Buffer 中。在未来数据被读取时,系统会将更改缓冲区中的数据合并恢复到 Buffer Pool 中,再将合并后的数据刷新到磁盘。

Change Buffer 的意义重大。与聚集索引不同,二级索引通常是非唯一的,并且在插入时往往呈现相对随机的顺序。同理,删除和更新操作也可能会影响索引树中不相邻的二级索引页。如果每次操作都直接访问磁盘,会产生大量的磁盘 I/O 开销。而有了 Change Buffer 后,这些数据变更可以先在缓冲池中进行合并处理,从而有效减少磁盘 I/O。

3)Adaptive Hash Index(自适应哈希索引)

自适应哈希索引用于优化对 Buffer Pool 中数据的查询操作。InnoDB 存储引擎会持续监控表上各索引页的查询情况。如果发现使用哈希索引能够提升查询速度,系统会自动建立哈希索引,即自适应哈希索引。

自适应哈希索引的建立无需人工干预,完全由系统根据实际情况自动完成。

4)Log Buffer(日志缓冲区)

日志缓冲区用于保存待写入磁盘的 log 日志数据,主要包括 redo log 和 undo log。其默认大小为 16 MB,日志缓冲区中的日志会定期刷新到磁盘。对于需要更新、插入或删除大量行的事务,适当增加日志缓冲区的大小可以有效节省磁盘 I/O 操作。

相关的参数:

- innodb_log_buffer_size(缓冲区大小)

- innodb_flush_log_trx_commit(日志刷新到磁盘时机):设置为 1,代表日志在每次事务提交时写入并刷新到磁盘;设置为 0,每秒将日志写入并每秒刷新到磁盘一次;设置为 2,日志在每次事务提交后写入,并每秒刷新到磁盘一次。

2.2 InnoDB 引擎 - 磁盘结构

1)undo Tablespaces(撤销表空间)

撤销表空间主要用于存储 undo log 日志。在 MySQL 实例初始化时,系统会自动创建两个默认的 undo 表空间,每个表空间的初始大小为 16 MB。

2)Temporary Tablespaces(临时表空间)

InnoDB 存储引擎使用了会话临时表空间和全局临时表空间,这些临时表空间用于存储用户创建的临时表等相关数据,为临时数据的存储和管理提供了专门的区域。

3)DoubleWrite Buffer Files(双写缓冲区)

双写缓冲区是 InnoDB 引擎保障数据可靠性的重要机制。在将数据页从 Buffer Pool 刷新到磁盘之前,InnoDB 引擎会先把数据页写入双写缓冲区文件。这样,当系统出现异常时,就可以利用双写缓冲区中的数据进行恢复,确保数据的完整性。

4)Redo log(重做日志)

重做日志的主要作用是实现事务的持久性。它由两部分构成:一是位于内存中的重做日志缓冲(redo log buffer),二是存储在磁盘上的重做日志文件(redo log)。当事务提交后,所有的修改信息都会被记录到重做日志中。在将脏页刷新到磁盘的过程中,如果发生错误,就可以借助重做日志中的信息进行数据恢复,保证数据的一致性和持久性。

以循环方式写入重做日志文件,涉及两个文件:ib_logfile0ib_logfile1

2.3 InnoDB 引擎 - 后台线程

将数据从缓冲区写入到磁盘中,会用到后台线程:

1)Master Thread(主线程)

Master Thread 作为核心后台线程,承担着调度其他线程的重要职责。同时,它负责将缓冲池中的数据异步刷新至磁盘,以此确保数据的一致性。其具体工作还涵盖了脏页刷新、插入缓存合并以及 undo 页回收等任务。

2)IO Thread(IO 线程)

InnoDB 存储引擎广泛运用 AIO(异步输入输出)技术来处理 IO 请求,这一举措能够显著提升数据库性能。IO Thread 的主要功能是处理这些 IO 请求的回调操作,保障 IO 操作的顺利完成。

3)Purge Thread(清除线程)

Purge Thread 主要用于回收已提交事务的 undo log。当事务提交后,对应的 undo log 可能不再需要,此时 Purge Thread 会将其回收,释放相关资源。

4)Page Cleaner Thread(页面清理线程)

Page Cleaner Thread 是协助 Master Thread 进行脏页刷新至磁盘的线程。它的存在有助于减轻 Master Thread 的工作负担,减少系统阻塞,提高整体性能。

3.0 InnoDB 引擎 - 事务原理概述

事务是一系列操作的集合,它构成了一个不可分割的工作单元。事务会将其中包含的所有操作视为一个整体,统一向系统提交操作请求或者撤销操作请求。也就是说,这些操作要么全部执行成功,要么全部执行失败,不存在部分成功部分失败的情况。

特性

  1. 原子性:事务是不可再细分的最小操作单元,其包含的所有操作要么全部成功执行,要么全部失败。原子性由 undo log(撤销日志)具体实现,undo log 会记录事务执行过程中的反向操作信息,若事务执行失败,可依据这些信息将数据恢复到事务开始前的状态。
  2. 一致性:当事务完成时,必须确保数据库中所有的数据都处于一致状态。一致性由 redo log(重做日志)和 undo log 共同实现。redo log 用于在系统崩溃等异常情况发生后,将未持久化的数据重新写入磁盘;undo log 则在事务执行过程中保证数据的中间状态是可恢复的,二者协同保证数据最终的一致性。
  3. 隔离性:数据库系统提供了隔离机制,该机制确保事务能够在不受外部并发操作影响的独立环境中运行。隔离性由 MVCC(多版本并发控制)和锁共同实现。MVCC 通过为数据的每个版本保留一个快照,使得不同事务可以同时访问不同版本的数据;锁则用于限制对数据的并发访问,防止数据冲突,二者结合保障事务的隔离性。
  4. 持久性:一旦事务完成提交或者回滚操作,它对数据库中数据所做的更改将是永久性的,不会因为后续的系统故障等原因丢失。持久性由 redo log 实现,redo log 会记录事务对数据所做的更改,即使数据库在事务提交后崩溃,也能依据 redo log 中的信息将数据恢复到一致状态。

3.1 InnoDB 引擎 - redolog

Redo log(重做日志)记录的是事务提交时数据页的物理修改情况,其主要作用是实现事务的持久性。

重做日志文件由两部分构成: 重做日志缓冲和重做日志文件。其中,重做日志缓冲位于内存中,而重做日志文件存储在磁盘上。当事务提交后,所有的修改信息都会被存入重做日志文件。在将脏页刷新到磁盘的过程中,若出现错误,就可以利用重做日志文件中的信息进行数据恢复。

具体流程

当客户端提交事务后,系统会先在内存结构的缓冲区中查找目标数据。若未找到,便会去磁盘中查找,并将找到的数据缓存到内存里。

若能直接在内存中定位到该数据,系统会直接在缓冲区对其进行修改。此时,该数据页就变成了脏数据页。

数据修改完毕后,重做日志缓冲会记录下修改后的数据,并通过后台线程将其写入磁盘上的重做日志文件。需要注意的是,修改后的数据页仍存于缓冲区,它会按照一定的时间规则被刷新到磁盘,而非在数据修改完成或事务提交后立即刷新。不过,对于日志而言,可通过设置参数,使事务提交后将内存中的重做日志缓冲即刻刷新到磁盘的重做日志文件中。

当脏数据页刷新到磁盘时,若能顺利写入 ibd 文件,重做日志文件中的相应数据便不再有用,会被循环覆盖,从而释放存储空间。

若脏数据页在刷新到磁盘的过程中出现错误,无法正常写入磁盘,就可借助重做日志文件进行数据恢复。恢复完成后,重做日志文件中的相关数据也会被循环覆盖,以释放存储空间。

相关概念

1)事务提交与数据查找

当客户端提交事务后,系统会遵循典型的数据库操作流程,优先在内存的缓冲区中查找目标数据。若未能找到,系统会从磁盘加载相应数据至内存,为后续操作做好准备。

2)脏数据页的概念

若在内存中成功定位到目标数据并完成修改,该数据页会被标记为“脏数据页”。所谓脏数据页,是指那些已经被修改,但尚未写回磁盘,数据状态与磁盘中数据不一致的数据页。

3)重做日志的记录

数据修改完成后,系统会迅速将修改后的数据记录到重做日志缓冲区。随后,通过后台线程将这些日志信息写入磁盘的重做日志文件。这一操作机制为系统在遭遇故障时提供了数据恢复的依据,有力地保障了数据的安全性。

4)脏数据页的刷新策略

脏数据页并不会在每次修改操作完成后就立即刷新到磁盘。相反,系统会依据一定的策略,如时间间隔、内存压力等因素,对脏数据页进行批量刷新。这种延迟刷新的方式有助于减少磁盘 I/O 操作,显著提高系统性能。

5)日志的刷新策略

对于重做日志,通常可通过配置相关参数,实现事务提交后立即将内存中的重做日志刷新到磁盘。这一操作能够确保日志的持久性,使系统在任何情况下都能准确记录数据的修改信息。

6)错误处理与恢复

在将脏数据页刷新到磁盘的过程中,若出现错误导致刷新失败,重做日志将发挥关键作用,用于恢复数据。这一机制是事务管理中的重要组成部分,能够有效确保数据的一致性和持久性,避免数据丢失或损坏。

7)重做日志的循环刷新

无论是脏数据页成功刷新到磁盘,还是借助重做日志完成数据恢复后,重做日志文件中的数据都会被循环刷新。也就是说,系统会定期清理或覆盖这些数据,以释放重做日志文件的存储空间,保证系统的高效运行。

3.2 InnoDB 引擎 - undolog

回滚日志主要用于记录数据在被修改之前的信息,它具备两个重要作用:一是支持事务回滚操作,二是服务于 MVCC(多版本并发控制)。

与记录物理日志的 redo log 不同,undo log 属于逻辑日志。可以这样理解,当执行 delete 操作删除一条记录时,undo log 会记录一条对应的 insert 记录;反之,当执行 insert 操作插入一条记录时,undo log 会记录一条对应的 delete 记录。当执行 update 操作修改一条记录时,undo log 会记录一条相反的 update 记录。当需要执行 rollback 操作时,系统可以从 undo log 的逻辑记录中读取相应内容,从而实现事务的回滚。

  • undolog 销毁:undo log 在事务执行过程中产生,但在事务提交时,并不会立即被删除。这是因为这些日志可能仍会被 MVCC 使用,以保证在并发环境下数据的一致性和隔离性。

  • undolog 存储:undo log 采用段的方式进行管理和记录,它被存放在前文提及的 rollback segment(回滚段)中。回滚段内部包含 1024 个 undo log segment,这种存储结构有助于高效地管理和组织回滚日志。

4.0 InnoDB 引擎 - MVCC 基本概念

MVCC(多版本并发控制,Multi - Version Concurrency Control)是数据库管理系统中一项重要的并发控制技术,它允许在数据库中多个事务能够同时对数据进行访问操作。

MVCC 的核心思想是为每个事务提供数据的多个版本。当数据发生修改时,系统不会直接覆盖原有的数据,而是创建一个新的数据版本,同时保留旧的数据版本。如此一来,正在读取旧版本数据的事务不会受到数据修改的影响,可以继续其操作流程;而新开启的事务则能够访问到最新的数据版本。

简而言之,MVCC 通过维护数据的多个版本,实现了读写操作的无冲突执行。其中,快照读为 MySQL 实现 MVCC 提供了非阻塞读的功能,大大提升了数据库在并发场景下的性能和效率。

MVCC 的基本概念

1)当前读

当前读所读取的是记录的最新版本。在进行读取操作时,为确保其他并发事务不会对当前记录进行修改,系统会对读取的记录进行加锁处理。在日常数据库操作中,诸如 select ... lock in share mode(使用共享锁)、select ... for update 语句,以及 updateinsertdelete 操作(使用排他锁)都属于当前读的范畴。

2)快照读

简单的 select 语句(不使用加锁操作)即为快照读。快照读读取的是记录数据的可见版本,这个版本有可能是历史数据,并且在读取过程中不会加锁,属于非阻塞读。不同的事务隔离级别下,快照读有着不同的表现:

  • Read Committed(读已提交):在该隔离级别下,每次执行 select 语句时,都会生成一次快照读,即每次读取都能看到已提交事务所做的修改。
  • Repeatable Read(可重复读):在开启事务后,首次执行的 select 语句才会触发快照读。在同一个事务内,后续的多次相同查询都会读取到相同的快照数据,保证了事务内查询结果的一致性。
  • Serializable(可串行化):在该隔离级别下,快照读会退化为当前读。这意味着所有的读取操作都会加锁,以确保事务串行执行,避免并发问题,但会降低系统的并发性能。

4.1 MVCC - 隐藏字段

记录中隐藏的字段:

1)DB_TRX_ID:最近修改事务 ID,记录插入这条记录或最后一次修改该记录的事务 ID。

2)DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合 undolog,指向上一个版本。

3)DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

4.2 MVCC - undolog 版本链

undolog:

回滚日志,在 insert、update、delete 的时候产生的便于数据回滚的日志。

当 insert 的时候,产生的 undolog 日志只在回滚时需要,在事务提交后,可被立即删除。而 update、delete 的时候,产生的 undolog 日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

undolog 版本链:

不同事务或相同事务对同一条记录进行修改,会导致该记录的 undolog 生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

生成 undolog 版本链具体过程:

  • 1)当事务 1 插入一条数据,提交完成之后,先会存放到内存缓冲区中:

DB_TRX_ID = 1,这是因为操作的事务的 ID 为 1。DB_ROLL_PTR = null,这是因为该数据是最新插入的数据,没有事务对其进行操作过。

  • 2)当事务 2 对该数据进行修改操作:

首先,undolog 会对修改之前的数据进行保存起来,再将数据进行修改:

其中 DB_TRX_ID 修改为事务 2 的 ID,DB_ROLL_PTR 指向之前的数据的地址,便于回滚或者便于每一个事务可以获取到该事务所对应的数据版本。

  • 3)当事务 3 也对该数据进行修改操作:

同理,将修改之前的数据存放到 undolog 中,再来修改数据:

将 DB_TRX_ID 修改为当前事务 ID 为 3,DB_ROLL_PTR 指针指向前一个数据的地址。

  • 4)当事务 4 也对该数据进行修改:

同理,将未修改的数据先存到 undolog 中,再来修改数据:

将 DB_TRX_ID 改为当前操作的事务 ID,DB_ROLL_PTR 指向前一个数据的地址。

4.3 MVCC - readview 介绍

ReadView(读视图)是快照读 SQL 执行过程中,MVCC(多版本并发控制)提取数据的重要依据。它会记录并维护系统当前处于活跃状态(即尚未提交)的事务 ID。

ReadView 包含四个核心字段,具体如下:

  1. m_ids:该字段存储了当前所有活跃事务的 ID 集合,它反映了在 ReadView 创建时刻,哪些事务还处于未提交状态。
  2. min_trx_id:表示当前活跃事务中的最小事务 ID。这个值有助于确定在进行数据版本可见性判断时的一个边界。
  3. max_trx_id:是预分配的事务 ID,其值为当前最大事务 ID 加 1。它为后续可能创建的新事务预留了 ID 空间,在判断数据版本的可见性时也起到重要作用。
  4. creator_trx_id:记录了创建该 ReadView 的事务的 ID。通过这个字段,可以明确 ReadView 与创建它的事务之间的关联关系。

版本链数据访问规则

  1. 事务 ID 匹配规则:检查 trx_id 是否等于 creator_trx_id。若满足此条件,意味着当前版本的数据是由当前事务进行更改的,当前事务能够访问该版本的数据。
  2. 事务 ID 小于最小活跃事务 ID 规则:判断 trx_id 是否小于 min_trx_id。若满足该条件,表明对应的数据所关联的事务已经提交,当前事务可以访问该版本的数据。
  3. 事务 ID 大于预分配事务 ID 规则:查看 trx_id 是否大于 max_trx_id。若满足此条件,说明产生该数据版本的事务是在 ReadView 生成之后才开启的,当前事务不能访问该版本的数据。
  4. 事务 ID 在范围内且不在活跃事务集合规则:判断是否满足 min_trx_id <= trx_id <= max_trx_id ,并且 trx_id 不在 m_ids 集合中。若满足该条件,当前事务可以访问该版本的数据。

不同隔离级别下 ReadView 的生成时机

  1. READ COMMITTED(读已提交)隔离级别:在该隔离级别下的事务中,每次执行快照读操作时,都会生成一个新的 ReadView。这使得事务在不同时刻进行快照读时,能看到其他已提交事务所做的最新更改。
  2. REPEATABLE READ(可重复读)隔离级别:在该隔离级别下的事务里,仅在第一次执行快照读操作时生成 ReadView,后续的快照读操作将复用这个 ReadView。这样可以保证在同一个事务内,多次快照读操作看到的数据版本是一致的,避免了不可重复读的问题。

4.4 MVCC - 原理分析(RC 级别)

READ COMMITTED:在事务中每一次执行快照读时生成 ReadView 。

举个例子:

接下来查看事务 5 查询所得到的数据版本:

首先,第一次快照的 ReadView:m_ids 集合中活跃事务有 3、4、5,事务 5 就是当前、min_trx_id 最小活跃事务 ID 为 3、max_trx_id 下一个事务 ID 为 6、creator_trx_id 创建该快照视图的事务为事务 5 。

接着,根据版本链数据访问规则:

之前所生成的版本数据链:

从最新版本的数据开始,向下逐一查找适合当前事务的版本数据。

当前最新版本数据的 trx_id 为 4。经检查,它不满足规则一(creator_id == trx_id),这表明该数据并非由当前事务创建;也不满足规则二,因为 min_trx 为 3,而 4 并不小于 3,说明产生该数据的事务尚未提交;同时,它也不满足规则四,因为 trx_id = 4 处于 m_ids 集合中。

接着查看下一个版本数据,其 trx_id = 3。此数据同样不满足规则一,因为 creator_id = 5,这意味着它不是当前事务创建的数据;不满足规则二,由于 min_trx 为 3,trx_id 与之相等,说明对应的事务尚未提交;也不满足规则四,因为 trx_idm_ids 集合中。

继续查看下一个版本数据,trx_id = 2。它不满足规则一,因为 creator_id = 5,即该数据不是当前事务创建的;但满足规则二,因为 min_trx 为 3,而 trx_id = 2,满足 trx_id < min_trx 这一条件。所以,在当前事务 5 中,第一次查询所获取的数据版本为 trx_id 为 2 的事务对应的版本。

对于第二次快照生成的 ReadView,查询时同样需结合该 ReadView 并依据版本数据访问规则进行操作,其查询流程与上述过程一致,在此就不再详细阐述了。

4.5 MVCC - 原理分析(RR 级别)

仅在事务中第一次执行快照读时,生成 ReadView,后续复用该 ReadView 。

这里只需要根据第一次执行的快照读所生成的 ReadView 结合版本数据访问规则来查询符合要求的版本数据,剩余的快照所得到的版本数据都是一样的。

4.6 小结

1)持久性(Durability)

持久性通过重做日志(Redo Log)得以实现。当事务成功提交后,系统会及时将该事务相关的修改记录写入重做日志。由于重做日志会持久地存储在磁盘上,即便系统遭遇崩溃等异常情况,也能够依据重做日志恢复已提交的事务,确保已提交事务所做的修改不会丢失,保证了数据修改的长久留存。

2)原子性(Atomicity)

原子性借助撤销日志(Undo Log)来达成。原子性的核心在于保证事务要么完整地执行成功(提交),要么彻底失败(回滚),不存在部分执行的情况。若事务在执行期间出现错误,可利用撤销日志将已完成的修改操作回滚至事务开始前的状态,以此确保事务操作的不可分割性和完整性。

3)一致性(Consistency)

一致性通过结合使用重做日志和撤销日志来实现。在事务的整个执行过程中,无论最终是提交事务还是回滚事务,数据库都必须保证其状态始终保持一致。在事务提交时,系统会应用重做日志来确认和持久化数据的修改;而在事务回滚时,则会使用撤销日志将数据恢复到事务开始前的状态。通过这种方式,确保了在事务的提交和回滚过程中数据的一致性。

4)隔离性(Isolation)

隔离性通过多版本并发控制(MVCC)和锁机制共同实现。多版本并发控制(MVCC)允许事务在进行读取操作时获取一个一致的数据视图,无需等待其他事务执行完毕,显著提高了数据库的并发处理能力。与此同时,锁机制发挥作用,在进行写操作时,能够阻止其他事务同时对同一数据行进行修改,从而有效保证了各个事务之间的隔离性,避免了不同事务之间的相互干扰和数据冲突。