本文MySQL版本是8.X版本
这是官方文档给出来的架构图:MySQL :: MySQL 8.0 Reference Manual :: 17.4 InnoDB Architecture
可以看出,整体上是分成两部分的:内存结构(提高效率)和磁盘结构(数据持久化),下面将把每个区域都大致做一个介绍。
内存结构
缓冲池(Buffer Pool)
缓冲池是用来缓存各种数据,最主要的就是缓存 从磁盘加载的 数据页,当数据库请求数据时,InnoDB首先查看缓冲池中是否存在所需的数据,如果存在,则直接从内存中读取,从而大幅提高读取速度。
结构
Instances
缓冲池至少有一个Instances实例对象。内存操作都是在Instances中进行的。
Instances的优点:
- 提升并发性能: 将Buffer Pool分成多个实例可以减少锁竞争,从而提高并发读取的效率。不同的连接可以并行地访问不同的Buffer Pool实例,减少了单一全局锁的压力。
- 优化内存管理: 每个Buffer Pool实例可以配置不同的大小,使得可以更好地控制内存的使用和分配。这对于大内存系统特别有用,可以有效地分配和管理大量的内存。
Instances数量相关:
- 通过系统变量 innodb_buffer_pool_instances 可以设置缓冲池实例的个数,默认是 1,最大值为 64。
- 当缓冲池的大小小于 1GB 时,无论指定 innodb_buffer_pool_instances 数是多少都会自动调整为 1。
- 当缓冲池的大小大于等于 1GB 时,默认的 innodb_buffer_pool_instances 值为 8,也可以指定大于 8的值来设置实例的数量。增加多个实例可以提升服务器的并发性能。
- 为了获得最佳效率,建议通过指定 innodb_buffer_pool_instances 和 innodb_buffer_pool_size,为每个缓冲池实例设置至少 1GB 的空间。
bash
# 查看instances的个数
show variables like "innodb_buffer_pool_instances";
Chunk
每个Instances实例中至少有一个Chunk块。每个Chunk中管理的是若干从磁盘加载到内存的Page数据页。
Chunk的优点
Chunk是在服务器运行状态下动态调整缓冲池大小时的操作单位。为了避免在调整大小过程中复制所有缓冲池中的数据页,调整操作以"块"为基本单位执行。
Chunk数量相关:
- Chunk大小可以通过系统变量innodb_buffer_pool_chunk_size进行设置,默认为134217728字节即128MB。在设置大小时,可以以1048576字节即1MB为单位增加或减少。
- 更改innodb_buffer_pool_chunk_size的值时需注意以下条件:
- 如果innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances大于当前缓冲池大小,innodb_buffer_pool_chunk_size将被截断为innodb_buffer_pool_size / innodb_buffer_pool_instances。
- 缓冲池大小必须始终等于或是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的倍数。如果修改了innodb_buffer_pool_chunk_size的值导致不符合这个规则,缓冲池在初始化时会自动四舍五入为最接近或者倍数于innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的值。
Page
数据页/缓冲页,就是磁盘中数据页的数据结构,读到内存后与磁盘中的对应。
页与页如何连接
InnoDB中为了实现数据页在内存中的链表连接,定义了一个称为"控制块"(control block)的数据结构,其中包含三个重要的信息:指向数据页的内存地址 前一个控制块的内存地址 后一个控制块的内存地址
为了确定控制块链表的起始位置,InnoDB专门定义了一个头节点,头节点中包含了三个主要的信息:第一个控制块的内存地址 最后一个控制块的内存地址 链表中控制块的数量
控制块和页初始化
①在缓冲池初始化过程中,会为每个Chunk(内存块)分配内存空间。控制块会从Chunk的内存空间的左侧向右侧进行初始化,而数据页所占的内存则从右侧向左侧初始化。这意味着控制块的内存空间分配优先于数据页的分配。缓冲池在初始化过程中,剩余的内存空间无法容纳一个完整的控制块和其对应的数据页,就会产生碎片化的空间。
②在内存初始化完成后,建立了控制块与缓冲数据页之间的关系。
③后面从磁盘加载数据页是,直接缓存到缓冲页中即可。
页的管理
当磁盘中的页缓存到数据页之后,如果对于页有 删除、修改操作,此时就是操作缓存的页。
缓冲池使用三个链表来维护缓冲页,这三个链表代表页在内存的三种状态。
Free List:管理未被使用的内存页,即空闲页。当执行查询操作时,如果所需的页已经在缓冲池中,则直接返回数据。如果不在缓冲池中且Free List不为空,则从磁盘中读取对应的数据页,并将其存放到Free List中的某一页中。随后,从Free List中移除这个页,并将其放入LRU List中。
LRU List:管理从磁盘读取到的数据页,包括未修改的(干净页)和已修改的(脏页)。根据LRU(最近最少使用)算法对链表中的页节点进行维护和淘汰。数据库刚启动时,LRU List是空的,所有从磁盘读取的页都存放在Free List中。当数据从磁盘读取到缓冲池时,首先从Free List中查找可用的空闲页。如果找到,则将该页从Free List中删除并加入LRU List;如果没有找到,则根据LRU算法淘汰LRU List末尾的页,并将该页的内存空间分配给新的数据页。
Flush List:当LRU List中的页被修改后(即成为脏页),会将其加入Flush List。Flush List专门用来管理需要被写回到磁盘的脏页。数据库通过刷盘机制将Flush List中的脏页刷回磁盘,以确保数据的持久性和一致性。刷盘操作完成后,将脏页的空间释放,并返回给Free List。
缓冲池使用LRU算法管理链表,当有新页面添加到缓冲池时,最近最少使用的页面将被淘汰,并将新页面添加到列表的中间。这种中点插入策略将列表视为两个子列表:
链表头部,是存放最近访问的新页面(年轻页面)子列表;
链表尾部,是存放最近较少访问的旧页面子列表。
经常使用的页面保存在新子列表中,较少使用的页面保存在旧子列表中。随着时间的推移,旧子列表中的页面将会逐渐被淘汰。默认情况下,算法的执行过程如下:
缓冲池总容量的 5/8 用于新子列表,3/8 用于旧子列表;
列表的中间插入点是新子列表的尾部与旧子列表头部的交界;
当一个页面被读入缓冲池时,首先插入到中点做为旧子列表的头节点;
当访问的页面在旧子列表中时,将被访问的页面移动到新子列表的头部,使其成为 "新" 页面;
数据库运行的过程中,缓冲池中被访问页面的位置不断更新,未访问的页面向列表的尾部移动,从而逐渐"变老",最终超出缓冲池容量的页面从旧子列表的尾部被淘汰。
缓冲池大小
bash
show variables like "innodb_buffer_pool_size";
#单位是字节 128M
InnoDB 会为控制块额外分配内存空间,因此实际分配的内存总空间比指定的缓冲池大小大约多出 10%。
增大缓冲池大小可以显著减少多次访问相同表数据时的磁盘 I/O 操作,因为数据已缓存在内存中,提高数据库的整体效率。不过,增大缓冲池大小可能会导致服务器在启动时需要更长的初始化时间。
缓冲池信息
bash
show engine innodb status\G
变更缓冲区(Change Buffer)
变更缓冲区用来缓存对二级索引 数据的修改,是一个特殊的数据结构。当使用 INSERT、UPDATE 或 DELETE 语句修改二级索引对应的数据时,如果对应的数据页在缓冲池中,则直接进行更新。如果数据页不在缓冲池中,修改操作 则会被缓存到变更缓冲区。这样就避免了立即从磁盘读取对应的数据页,而是等到未来的读操作将数据页加载到缓冲池时,变更缓冲区中的修改操作可以批量合并到缓冲池中,从而减少磁盘 I/O 的次数,提升系统性能。
合并修改执行时机:
读取对应的数据页时: 当系统需要读取某个数据页时,如果这个页上有变更缓冲区的待合并修改操作,系统会先将变更缓冲区的修改合并到数据页中,然后再将数据页加载到缓冲池中。上图情况。
系统空闲或 Slow Shutdown 时: 在系统空闲或者进行缓慢关闭(Slow Shutdown)时,MySQL的主线程会启动合并操作,将变更缓冲区的修改批量合并到对应的数据页,以减少后续读取时的磁盘 I/O 操作。
Change buffer 的内存空间即将耗尽时: 如果变更缓冲区的内存空间即将用尽,MySQL会触发合并操作,将缓冲区中的修改写入到对应的数据页中,释放变更缓冲区的内存空间。
Redo Log 写满时: Redo Log 是MySQL中用于持久化记录事务修改的重要组成部分。如果Redo Log写满,MySQL可能会暂停事务处理并等待写入完成。此时,变更缓冲区的修改可能会被迫立即写入数据页,以释放Redo Log空间并确保事务持久性。
为什么是二级索引?
由于聚集索引具有唯一性,这意味着每个主键值只能存在一次。考虑以下情况:假设表中有一个主键(ID),现在有两条 INSERT 语句尝试插入相同的主键值(例如,id=1)。如果这两个操作都被放入变更缓存中,待合并到缓冲池时,会出现重复的主键值。这违反了聚集索引的唯一性约束,因此聚集索引的修改不能简单地放入变更缓存中。
与聚集索引不同,二级索引通常不具有唯一性,并且它们的数据分布相对随机。对于二级索引的插入、删除和更新操作,可能会涉及到不相邻的索引页。如果每次操作都需要直接从磁盘读取数据,会导致大量的随机 I/O 操作,影响系统性能。因此,通过将这些修改操作暂时缓存到变更缓存区,待真正读取数据时再将修改合并到缓冲池中,可以显著提升效率,减少随机 I/O 的次数。
修改变更缓冲区
- 缓冲类型和控制
在修改二级索引数据时,变更缓冲区可以显著减少磁盘I/O操作,从而提高数据库的效率。然而,变更缓冲区占用了缓冲池的一部分空间,可能会影响可用于缓存数据页的内存。在业务场景中,如果读操作远多于写操作,或者表中二级索引较少,考虑禁用变更缓冲区可能有助于提高缓冲池的可用空间。
可以通过选项文件或使用 SET GLOBAL 语句来配置系统变量 innodb_change_buffering,以控制变更缓冲区对插入、删除操作(索引记录标记删除)、清除操作(索引记录物理删除)的开启或禁用:
all:默认值,缓存插入、删除标记和清除操作。
none:不缓存任何操作。
inserts:仅缓存插入操作。
deletes:仅缓存删除标记操作。
changes:缓存插入和删除标记操作。
purges:缓存后台执行的物理删除操作。
- 更改缓冲区的最大大小
通过系统变量 innodb_change_buffer_max_size 可以设置变更缓冲区的最大大小,默认为 25,最大可设置为 50,表示变更缓冲区占用缓冲池总内存的百分比。
在大量插入、更新和删除操作频繁的业务场景中,考虑增加 innodb_change_buffer_max_size 的值。
在读多写少的场景中,例如静态数据用于报表的场景,可以考虑减小 innodb_change_buffer_max_size 的值。
需要注意的是,如果变更缓冲区占用了过多的缓冲池内存空间,可能会导致缓冲池中的数据页更快被淘汰。
缓冲区信息
bash
show engine innodb status\G
自适应哈希索引(Adaptive Hash Index)
自适应哈希索引使得InnoDB存储引擎在不牺牲事务特性、可靠性和缓冲池空间的前提下提升效率,使其更像是一个内存数据库。
哈希索引根据经常访问的索引页自动 构建。根据InnoDB内部 的监控机制,如果监测到某些查询可以通过建立哈希索引提高性能,则会自动对这些页创建哈希索引。这个过程被称为自适应哈希索引。
当表完全放在内存中时,哈希索引可以通过直接查找任何元素来加快查询速度。
查询条件为key,页地址为value
自适应哈希索引(AHI)会占用缓冲池的一部分内存区域,并在缓冲池初始化后进行初始化。为了减少AHI对锁竞争的压力,AHI 支持分区。你可以通过设置 innodb_adaptive_hash_index_parts 参数来配置分区的个数,默认值为8。
通过设置系统变量 innodb_adaptive_hash_index 可以开启或关闭自适应哈希索引:
在选项文件中设置 innodb_adaptive_hash_index=[1|0] 可以实现开启或关闭。
通过命令行选项 --skip-innodb-adaptive-hash-index 也可以关闭自适应哈希索引。
每个自适应哈希索引被绑定到不同的分区中,每个分区有不同的锁保护。分区的数量由系统变量 innodb_adaptive_hash_index_parts 控制,默认值为 8,最大可配置为 512。
日志缓冲区(Log Buffer)
日志缓冲区是在服务器启动时向操作系统申请的一片连续的内存区域,用来存储即将要写入磁盘日志文件的数据。
在进行数据库的数据操作语言(DML)操作时,InnoDB会记录这些操作的日志,例如为了保证数据完整性而实现的重做日志(Redo Log)。这些日志首先会写入日志缓冲区(Log Buffer)。这样做可以解决由于同步写磁盘而导致的性能问题,因为将日志首先写入内存中的日志缓冲区通常比直接写入磁盘要快速。
随后,根据不同的落盘策略(例如 InnoDB 的 Checkpointing 过程),最终会将日志从日志缓冲区写入到磁盘中的日志文件,确保数据在持久化存储中的安全性和完整性。
磁盘结构
系统表空间(System Tablespace)
作用
存储系统表和数据字典:系统表空间存储了MySQL中所有系统表的数据,包括数据字典。这些数据对于数据库的正常运行和元数据管理至关重要。
变更缓冲区存储: 当数据库服务器关闭时,系统表空间还保存了没有合并到缓冲池的二级索引修改操作。这些修改在下次数据库启动时将被应用。
在MySQL 8.0.20之前的版本中,系统表空间还包含了双写缓冲区。从MySQL 8.0.20开始,双写缓冲区已经被移出系统表空间,而是存储在一个单独的文件中。
配置选项
系统表空间可以对应一个或多个数据文件。默认情况下,MySQL在 data 目录中创建一个系统表空间数据文件 ibdata1。系统表空间数据文件的大小和数量由 innodb_data_file_path启动选项定义。所以我们可以从这个选下下手来修改配置。
在修改系统表空间配置时,先停止MySQL服务,修改完成后,再重新启动MySQL服务之后生效。
查看文件
bash
show variables like "%innodb_data_file_path%";
file_name:file_size[:autoextend[:max:max_file_size]]
file_name: 文件名,用于标识数据文件。
file_size: 文件大小,以 K、M 或 G 为单位指定。如果以 K 为单位,必须是 1024 的倍数;否则,四舍五入到最接近的 M(兆字节),且至少为 12MB。
autoextend: 可选属性,指定数据文件是否自动扩展。默认情况下,每次增加 64MB。可以通过系统变量 innodb_autoextend_increment 控制增量大小。
max: 可选属性,仅适用于最后指定的数据文件,指定数据文件的最大容量。例如,max:500M 表示文件最大可以扩展到 500MB。
示例配置:
mysqld节点
[mysqld]
文件1名称为:ibdata1,大小为50M
文件2名称为:ibdata2,大小为50M,自动扩容
innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend
配置系统表空间文件路径:
mysqld节点
[mysqld]
指定innodb数据目录
innodb_data_home_dir = /myibdata/
配置系统表空间
innodb_data_file_path=ibdata1:50M:autoextend
注意事项:
指定 innodb_data_home_dir 时,路径必须以斜杠 / 结尾,MySQL 不会自动创建目录,需确保目录存在 。
如果 innodb_data_file_path 指定了绝对路径,则不会使用 innodb_data_home_dir 的值。
在添加新的数据文件时,不要指定已存在的文件名,InnoDB 在启动服务器时会自动创建并初始化新的数据文件。
独立表空间(File-Per-Table Tablespace)
作用
File-Per-Table 表空间是 InnoDB 存储引擎的一种设置,默认情况下,它将每张表的数据和索引存储在单独的数据文件中,这些文件被称为表空间数据文件。这种配置方式使得每个表的数据管理更为灵活和独立,有助于提高维护性和性能调整的精度。
配置选项
默认情况下,每张表都对应一个独立的表空间数据文件。这种设置可以通过系统变量 innodb_file_per_table 控制,该变量的值可以是 ON 或 OFF,用来决定是否为每张表生成独立的表空间文件。
选项文件设置:
在 MySQL 的选项文件(如 my.cnf)中,可以通过配置 [mysqld] 节点来设置 innodb_file_per_table 的值。例如:
[mysqld]
innodb_file_per_table=ON
这表示开启 File-Per-Table 表空间,每张表都有自己的表空间数据文件。
运行时设置:
可以通过 MySQL 的 SET GLOBAL 语句在运行时修改 innodb_file_per_table 的值。例如:
mysql> SET GLOBAL innodb_file_per_table=ON;
这会立即将系统配置切换为使用 File-Per-Table 表空间。
优缺点
优点:
- 磁盘空间回收:使用 TRUNCATE 或 DROP 表时,File-Per-Table 表空间会将磁盘空间返回给操作系统,提高磁盘利用率。而共享表空间(如 System Tablespace)则无法回收磁盘空间。
- 性能优化:执行 TRUNCATE TABLE 操作时,File-Per-Table 表空间通常有更好的性能表现,因为它不需要处理共享表空间的复杂性。
- 灵活的存储位置:可以在其他目录或独立存储设备上创建 File-Per-Table 表空间文件,以达到 I/O 优化、空间管理或备份等目的。可以通过 DATA DIRECTORY 子句指定表的数据目录,使表数据存储在外部目录中。
- 支持动态和压缩行格式:File-Per-Table 表空间支持更多的行格式选项,如 DYNAMIC 和 COMPRESSED,而共享表空间不支持这些选项。
- 数据恢复和备份:在发生数据损坏、备份不可用或 MySQL 服务器实例无法重新启动时,File-Per-Table 表空间可以提高成功恢复的机会。
- 大表支持:单个表的容量限制为 64TB,相比之下,共享表空间中的表总容量也是 64TB,但 File-Per-Table 可以更灵活地管理单个表的大容量数据。
缺点:
- 未使用空间管理:每个表可能存在未使用的空间,这些空间只能由对应的表使用,可能导致空间浪费,特别是管理不当时。
- 文件描述符和性能:每个表有自己的数据文件,操作系统需要维护更多的文件描述符。当表数量很多时,可能会对性能产生一定影响。
- 磁盘碎片问题:可能会导致更多的磁盘碎片,特别是在执行 DROP TABLE 操作时,可能会影响性能。
- 自动扩展限制:对于 File-Per-Table 表空间文件,系统变量 innodb_autoextend_increment 定义的自动扩展增量不起作用。File-Per-Table 表空间文件会始终自动扩展,初始大小由表定义决定,之后以 4MB 为增量进行扩容。
通用表空间(General Tablespace)
通用表空间(General Tablespace)是MySQL 8.0 引入的一种表空间类型,它与传统的 File-Per-Table 表空间有所不同。通用表空间允许将多个表存储在单个共享表空间文件中,相比之下,File-Per-Table 表空间每个表有自己的数据文件。
临时表空间(Temporary Tablespace)
临时表存储临时数据,通常用于复杂查询或计算过程中存储过渡的中间结果。MySQL在执行查询和计算时会自动生成临时表,例如在表连接查询时得到的结果集就是一张临时表,因为结果中可能包含多个表中的字段,并没有一张真实的表与之完全对应。
外部临时表:
用户可以使用 CREATE TEMPORARY TABLE 语句手动创建临时表。
这些表只在当前会话中可见,并且在会话关闭时会自动删除。
内部临时表:
服务器在执行特定操作时自动创建的临时表。
用户无法直接控制这些表的创建,它们通常用于以下情况:
使用 UNION 合并查询结果
对视图执行操作,如使用 UNION 或聚合函数
使用子查询
使用 DISTINCT 和 ORDER BY 的查询可能需要一个临时表
使用 INSERT...SELECT 将查询结果插入表中时,需要一个内部临时表来保存查询结果行
使用 COUNT(DISTINCT) 和 GROUP_CONCAT() 表达式
使用窗口函数时
撤销表空间(Uodo Tablespace)
MySQL在初始化时会在数据目录下创建两个默认的撤销表空间,其数据文件名分别为 undo_001 和 undo_002。这些撤销表空间在数据字典中的名称分别为 innodb_undo_001 和 innodb_undo_002。
作用
撤销表空间的主要作用是记录事务对聚集索引记录的修改,包括新增、修改和删除操作。通过记录这些修改,当事务需要回滚时,数据库系统可以根据撤销日志中的信息,将数据库状态恢复到事务开始之前的状态,从而确保事务的原子性。
事务回滚:当事务由于错误或者其他原因需要回滚时,撤销表空间记录的信息允许数据库系统撤销事务对数据的任何更改。
并发控制:撤销表空间也是实现数据库的并发控制机制的重要组成部分,确保事务之间的隔离性和一致性。
撤销日志(Undo Log)
撤销日志(Undo Log)是事务处理过程中的关键机制,用于确保事务的原子性。每当事务对数据进行修改时,系统会在磁盘上创建相应的撤销日志。
撤销日志写入时机
撤销日志在表空间组织形式
撤销表空间中包含回滚段,每个回滚段中包含若干个撤销槽(undo slots),每个槽对应一个撤销日志段(Undo log segments)。撤销日志段中包含具体的撤销日志。
撤销日志段(Undo log segments):也称为撤销段,每个撤销日志段可以保存多个事务的回滚日志。不过同一时刻只能被一个活跃事务使用,其占用的空间只能在事务提交或回滚后重新使用。
回滚段(Rollback segments):包括撤销日志段,通常位于撤销表空间和全局临时表空间中。可以通过系统变量innodb_rollback_segments定义分配给每个撤销表空间和全局临时表空间的回滚段数量,默认值为128,取值范围为[1, 128]。
回滚段支持的事务数:取决于回滚段中的撤销槽数量以及每个事务所需的撤销日志数。撤销段中的撤销槽数量可以根据InnoDB页大小计算,公式为(InnoDB页大小 / 16)。例如,对于默认的InnoDB页大小为16KB,一个回滚段可以包含1024个撤销槽,用于存储事务的撤销日志。
回滚段中的其他记录信息:回滚段还记录了History List的头节点(History List Base Node)和回滚段的大小。
通过系统变量配置:可以使用系统变量innodb_rollback_segments设置撤销表空间中的回滚段数量,最大值和默认值均为128。
撤销日志组织形式及其内容
UNDO PAGE HEADER:记录Undo Log页的类型、日志偏移位置、下一个页链表引用等信息。
UNDO LOG SEGMENT HEADER:记录回滚段的信息。
UNDO LOG HEADER:记录产生这条日志的事务ID(Trx ID)、事务的提交顺序(Trx No)以及其他事务相关信息
Undo Log页的其他空间用来记录Undo Log日志。如果某个事务很大,一个Undo Log页无法完整记录,就需要申请新的Undo Log页。新页通过UNDO PAGE HEADER中的链表引用信息链接到前一个页。后续的页只需要记录Undo Log,不需要记录Undo头信息。
这由Undo Log构成的链表称为Undo链,在事务中扮演着非常重要的角色。
Uodo链分类
Insert Undo链组织方式:每个Insert Undo链中的Undo Log对应一条新的数据行。数据行中使用ROLL_POINTER信息来关联Undo Log。在回滚操作时,可以通过ROLL_POINTER信息找到需要回滚的Undo Log。
Update Undo链组织方式:对于删除和更新操作,在Update Undo链中,每个Undo Log都对应一个数据行。每次更新都通过Undo Log中的ROLL_POINTER进行关联,因此每个数据行形成一个Undo Log版本链。在事务的MVCC(多版本并发控制)中,这个版本链起着非常重要的作用,用于解决事务的"隔离性"。回滚操作可以依序撤销这些版本,确保数据在不同事务间的一致性和隔离性。
撤销日志分类
- 新增 (TRX_UNDO_INSERT_REC):Undo Log操作信息相对简单,只记录了主键值、主键长度等主键信息。
- 删除 (TRX_UNDO_DEL_MARK_REC):除了记录主键信息外,还记录了:
- 旧的事务ID
- 旧的ROLL_POINTER信息
- 用来构建有序Undo版本链的信息
- 一些被索引字段的信息
- 更新 (TRX_UNDO_UPD_EXIST_REC)
- 如果更新操作没有修改主键:
类似于删除操作,记录主键信息、旧的事务ID、旧的ROLL_POINTER信息、被索引字段的信息和被更新的信息。 - 如果更新操作修改了主键:
记录两条Undo Log:
一条删除的Undo Log
一条新增的Undo Log
- 如果更新操作没有修改主键:
内存中的撤销日志
与数据页在内存中的保存方式相同,撤销日志也保存在Buffer Pool中,其结构与磁盘中的UndoLog页一致。每个UndoLog页在内存中通过控制块进行管理。
在内存中,使用四个链表来管理正在使用的UndoLog页和空闲UndoLog页,根据不同的日志类型分为:
Insert List:正在被使用的,用来管理Insert类型的UndoLog页。
Insert Cache List:空闲的或者被清理后可以被后续事务重用的Insert类型UndoLog页。
Update List:正在被使用的,用来管理Update类型的UndoLog页。
Update Cache List:空闲的或者被清理后可以被后续事务重用的Update类型UndoLog页。
重做日志(Redo Log)
重做日志用于确保在数据库崩溃后 能够恢复已经提交但尚未写入到磁盘的事务数据。重做日志以文件形式存储在磁盘上。在正常操作过程中,MySQL会根据受影响的记录生成并写入重做日志文件中的数据,这些记录被称为"Redo"信息。
当数据库重新启动时,系统会自动读取重做日志来执行数据恢复操作。这样可以确保即使在系统崩溃或异常断电等情况下,已提交的事务能够被正确恢复,从而保证数据的一致性和持久性。
为什么不直接写入磁盘?
当执行数据操作语言(DML)操作时,所有修改都应包含在事务中。一旦修改完成并提交事务,内存中的修改数据页就应刷新到磁盘以实现持久性。
如果在此DML操作中开始刷新到磁盘时服务器崩溃 ,未刷新到磁盘的数据页将会丢失,导致事务在磁盘上的修改不完整,从而破坏了事务的一致性。
为解决此问题,InnoDB在每次执行DML操作时,会将内存中修改的数据页内容首先以日志形式记录到磁盘上 ,然后再真正落盘。这种方式相当于对修改进行了一次备份,即使服务器崩溃,也能从磁盘上的日志文件中找到上次崩溃前未落盘的数据,并完成落盘操作。
InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging),其基本思想是先写日志,再写磁盘。只有日志写入成功,事务才能算作提交成功。这里的日志即为重做日志(Redo Log)。在发生宕机且数据未刷到磁盘时,可以通过重做日志来恢复数据,确保ACID特性中的持久性。这也是重做日志的作用所在。
重写日志写入时机
在数据修改操作发生时 ,重做日志会被追加记录。已经落盘的数据对应的日志位置被记录为检查点,标记检查点之前的数据为无效。这样设计使得重做日志文件可以循环使用。
Mini-Transaction
上面说到只要是DML操作就会先写到重做日志中,但是如果这个记录记录到一半时系统奔溃了又该怎么办?所以要有个机制来保证日志必须时完整的,否则就不按照不完整的日志进行恢复。
Mini-Transaction(MTR)是针对DML操作过程定义的概念,指的是记录一个DML操作所产生的一组完整日志。在MySQL中,这组Redo Log作为一个不可分割的整体用于崩溃恢复。这里所说的不可分割的组包括:
向聚簇索引对应的B+树页面中插入一条记录时产生的Redo Log不可分割;
向某个二级索引对应的B+树页面中插入一条记录时产生的Redo Log不可分割;
其他对页面进行访问操作时产生的Redo Log不可分割。
Mini-Transaction(MTR)包含一个DML操作所对应的一组Redo Log。一个事务可能包含多个DML操作,因此一个事务中可以包含一个或多个SQL语句。每个SQL语句可能包含一个或多个MTR,而每个MTR则包含一条或多条Redo Log。
InnoDB为了尽可能节省空间,在MTR只包含一条日志的情况下,进行了优化。日志类型只有几十种。用于表示日志类型的TYPE字段长度为1字节,其中只用了7个比特位,能够表示整数127,足以表达所有的日志类型。因此,可以通过省出一个比特位来表示当前MTR包含一条还是一组Redo Log。换句话说,如果TYPE字段的第一个比特位为1,表示MTR只包含一条Redo Log;为0表示MTR包含一组Redo Log。
重写日志格式
Redo Log本质上记录了事务对数据库所做的修改操作,涵盖了多种场景,如数据行和索引页的增删改,以及范围修改和删除等。不同类型的redo日志定义了不同的结构,但大部分类型的redo日志通常包含以下通用结构:
Type: 日志类型,1字节
Space ID: 操作所属的表空间,4字节
Page number: 操作的数据页在表空间中的编号,4字节
Data: 日志的具体内容,长度不固定
管理重做日志数据结构
用来组织Redo Log的数据结构是Redo页,页的大小是512字节,也可以称为一个Redo Log Block。这个大小恰好对应磁盘上的一个扇区,这样设计可以保证在将日志写入磁盘时的连续性。
LOG_BLOCK_HDR_NO:Block的唯一标识,是一个大于0的值,取值范围是1到0x40000000UL。0x40000000UL对应的整数是1073741824,即1GB。因此,InnoDB最多能够生成1GB个日志块。每个日志块大小为512字节,所以InnoDB允许维护的最大容量为512GB。
LOG_BLOCK_HDR_DATA_LEN:表示Block中已经使用了多少字节。由于块头占用了12字节的空间,所以初始值为12。当Log Block Body被全部写满时,这个值就是512。
LOG_BLOCK_FIRST_REC_GROUP:如果一个MTR会生成多条redo日志记录,这些日志记录被称为一个redo日志记录组。LOG_BLOCK_FIRST_REC_GROUP代表该Block中第一个MTR中第一条日志的偏移量。
LOG_BLOCK_CHECKPOINT_NO:表示检查点的编号。
LOG_BLOCK_CHECKSUM:表示Block的校验和,用于正确性校验。
Redo页的组织形式
写入是从缓冲区的第一个 Redo Log Block 的 Log Block Body 开始,依次向后写入。每个 Redo Log Block 的大小为固定的512字节。
当一个 Block 的空间用完之后,接着写入下一个 Block。
InnoDB 提供了一个名为 buf_free 的全局变量,该变量表示后续写入日志在 Log Buffer 中的起始位置。
重做日志的刷盘时机
在InnoDB中,当一个MTR执行完成后,其生成的Redo Log会被写入到Log Buffer中。由于Log Buffer的大小是有限的,并且其中记录的Redo Log的目的是为了在服务器崩溃后进行数据恢复,因此将其保存在内存中是不安全的。因此,InnoDB会在以下情况将Redo Log刷写(flush)到磁盘:
Log Buffer 空间不足时:Log Buffer的大小通过系统变量 innodb_log_buffer_size 设置,当当前Log Buffer中的Redo Log占用了总容量的一半左右时,会触发将部分Redo Log刷写到磁盘。
事务提交时:当事务提交时,事务对应的MTR已经完全记录在Log Buffer中。在数据真正落盘之前,需要将这部分Redo Log刷新到磁盘,以确保事务的持久性。
后台线程定时刷盘:后台的Master Thread线程大约每秒会将Log Buffer中的Redo Log定期刷新到磁盘,这种方式保证了持久化操作的及时性和效率。
正常关闭服务器时:在正常关闭服务之前,会将Log Buffer中的所有Redo Log刷新到磁盘,以确保所有未持久化的操作得到保存。
做检查点(checkpoint)操作时:检查点操作是指将数据库中的所有已修改的数据页写入到磁盘。在执行检查点时,会包括将Log Buffer中的部分或全部Redo Log刷写到磁盘,以保证数据库的一致性和持久性。
刷盘策略在InnoDB中可以通过多种系统变量进行配置,确保数据的持久性和性能之间的平衡。以下是关于刷盘策略的相关配置:
innodb_flush_log_at_trx_commit:该系统变量控制事务的Redo Log何时写入磁盘。
0:每秒将Redo Log写入系统缓冲区,但不立即刷新到磁盘。在MySQL崩溃时可能会丢失最多1秒的事务。
1:每次事务提交时,将Redo Log写入系统缓冲区并立即刷新到磁盘,确保事务持久性。
2:每次事务提交后,将Redo Log写入系统缓冲区,但每秒刷新一次到磁盘。在MySQL崩溃时可能会丢失最多1秒的事务。
Innodb_flush_log_at_timeout:如果设置了该系统变量,可以指定Redo Log在系统缓冲区中的停留时间(以秒为单位),超过这个时间会强制刷新到磁盘。默认情况下,这个值是1秒。
sync_binlog:如果启用了二进制日志(binary log),并且设置了sync_binlog = 1,则必须将innodb_flush_log_at_trx_commit设置为1,以确保二进制日志与InnoDB事务的一致性。
这些配置选项允许数据库管理员根据应用程序的需求来调整性能和数据安全之间的权衡。
重做日志分类
用于数据页的日志类型:
这类日志记录了对数据页(如表的数据行或索引页)的修改操作。它们包括了对数据行的插入、更新、删除,以及索引页的修改等。
用于表空间文件的日志类型:
此类日志记录了对整个表空间文件的修改,例如对表空间的创建、扩展、压缩或者重命名等操作。
提供额外信息的日志类型:
这类日志提供了关于事务、恢复或其他元数据的附加信息,如检查点信息、事务提交记录等。
重做日志如何进行奔溃恢复
获取最新的 checkpoint_lsn 和恢复的起点:最新的 checkpoint_lsn 可以从 RedoLog 文件组的第一个文件的管理信息中的 checkpoint1 和 checkpoint2 block 中获取。这两个 block 中存储了 checkpoint_lsn 和 checkpoint_no 信息。比较这两个 block 中的 checkpoint_no 值,较大的那个对应着最新的 checkpoint 信息。
通过上述步骤,您可以获得最新发生的 checkpoint 的 checkpoint_lsn 值及其在 RedoLog 文件组中的偏移量 checkpoint_offset。
确认恢复的终点:RedoLog 是顺序写入的,每个 block 的头部有 LOG_BLOCK_HDR_DATA_LEN 属性记录当前 block 使用了多少字节。对于写满的 block,这个值是 512 字节。
找到第一个 LOG_BLOCK_HDR_DATA_LEN 不为 512 的 block,这个 block 中的最后一条日志就是恢复的终点。
进行恢复:确定了需要恢复的起点和终点后,需要顺序扫描 checkpoint_lsn 之后的日志记录。
对于每条日志记录,根据其内容依次恢复对应的数据页。
InnoDB 在恢复过程中采用了优化措施,使用哈希表将相同 Space Id 和 Page No 的日志记录组织在一起,以减少读取数据页时的随机 I/O 次数。
双写缓冲区(Doublewrite Buffer Files)
双写缓冲区是磁盘上的一个存储区域,用于增强InnoDB的数据持久性和可靠性。具体来说,在InnoDB将缓冲池中的数据页写入到磁盘上表空间数据文件之前,会先将对应的页写入双写缓冲区。这样做的目的是为了在数据真正落盘的过程中,例如操作系统或存储子系统崩溃或异常断电时,能够保证数据的完整性和一致性。
如果在数据写入双写缓冲区后,系统发生了崩溃或异常情况,InnoDB在崩溃恢复时可以从双写缓冲区中找到一份完好的页副本。这种机制有效地减少了数据损坏或丢失的风险,提高了数据库的可靠性和恢复能力。