MySQL - [11] InnoDB存储引擎
Page 页。是真正理解InnoDB存储引擎的入口。
一、Page —— 页
1.1、InnoDB 数据页及其结构
- 为了避免一条一条读取磁盘数据,InnoDB采取页的方式,作为磁盘和内存之间交互的基本单位,一个页的大小一般是16KB。
- InnoDB 为了不同的目的而设计了多种不同类型的页、存放undo日志信息的页等等。
- 我们把存放表中数据记录的页,称为 索引页 or 数据页。往数据页(User Records部分)中存储数据(也叫“记录”)。
查看innodb_page_size大小:
show variables like 'innodb_page_size';
(value的值单位为byte)
1.2、记录的头信息
>> deleted_flag
:逻辑删除标记(0:未删除,1:已删除)
>> min_rec_flag
:B+树中每层非叶子节点中的最小的目录项记录,都会添加该标记。
>> n_owned
:一个页面被分若干组后,“带头大哥”用于保存组中所有的记录条数。
>> heap_no
:表示当前记录在页面堆中的相对位置。
>> record_type
:表示当前的记录类型
① 0:普通记录
② 1:B+数非叶子节点的目录项记录
③ 2:表示Infimum记录
④ 3:表示Supremum记录
>> next_record
:表示下一条记录的相对位置,也就是链表。它表示从当前记录的真实数据到下一条的真实数据的距离。
1.3、Page Directory概述
记录在页中是按照主键值从小到大的顺序串联称为一个单向链表,因此查询也只能以头节点开始逐一向后查询,但是如果数据量很大,那么性能就无法保证了。针对这个问题,InnoDB采取了图书目录的解决方案,即:Page Directory。
分组规则如下所示:
① 对于Infimum记录所在的分组只能有1条记录。
② 对于Supremum记录所在的分组只能在1~8条记录之间。
③ 剩下的其他记录所在的分组只能在4~8条记录之间。
分组步骤如下:
① 初始情况下,一个数据页中只有Infimum记录和Supremum记录这两条,所以分为两个组。
② 之后每当插入一条记录时,都会从页目录中找到对应记录的主键值比待插入记录的主键值大,并且差值最小的槽,然后把该槽对应的n_owned加1.
③ 当一个组中的记录数等于8时,当再插入一条记录的时候,会将该组中的记录拆分成两个组(一个组中4条记录,另一个组中5条记录)。并在拆分过程中,会在Page Directory中新增一个槽,并记录这个新增分组中最大的那条记录的偏移量。
二、Index —— 索引
如何加快记录的查询?
当记录越来越多,创建的页也会越来越多,如果仅通过链表方式遍历查询,性能会出现很大问题。如何解决?采用B+树结构,即:叶子节点里存储完整的数据(数据页),非叶子节点存储主键索引(索引页)。
三、Buffer Pool —— 缓冲池
3.1、缓冲池概述
为了缓存磁盘中的页,MySQL服务器启动时就向操作系统申请了一片连续的内存空间,他们给这片内存起名为Puffer Pool(缓冲池)。默认Buffer Pool只有128M,可以在启动服务器的时候配置innodb_buffer_pool_size
(单位为字节)启动项来设置自定义缓冲池大小。Buffer Pool对应的一片连续的内存被划分为若干个页面,默认也是16kb,该页面称为缓冲页。为了更好地管理Buffer Pool中的这些缓冲页,InnoDB为每个缓冲页都创建了控制块,它与缓冲页是一一对应的。
3.2、Free链表
Buffer Poll的初始化过程中,是先向操作系统申请连续的内存空间,然后把它划分成若干个【控制块&缓冲页】对儿。当插入数据的时候,为了能够知道哪些缓冲页是空闲且可分配的,MySQL把所有空闲的缓冲页对应的控制块作为一个节点放到一个链表中,这个链表便称之为Free链表。
3.3、Flush链表
如果我们修改了Buffer Pool中某个缓冲页的数据,那么它就与磁盘上的页不一致了,这样的缓冲页也被称之为脏页(dirty page)。为了性能问题,我们每次修改缓冲页后,并不着急立刻把修改刷新到磁盘,而是将被修改过的缓冲页对应的控制块作为节点加入到这个链表中,该链表也被称为flush链表。
Flush链表刷新方式有如下两种:
【1】从flush链表中刷新一部分页面到磁盘
>> 后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是否繁忙。—— 即:BUF_FLUSH_LIST
>> 有时后台线程刷新脏页的进度比较慢,导致用户准备加载一个磁盘页到Buffer Pool中时没有可用的缓冲页。此时,就会尝试查看LRU链表尾部,看是否存在可以直接释放掉的未修改缓冲页。如果没有,则不得不将LRU链表尾部的一个脏页同步刷新到磁盘(与磁盘交互是很慢的,这会降低处理用户请求的速度)。 —— 即:BUF_FLUSH_SINGLE_PAGE
【2】从LRU链表的冷数据中刷新一部分页面到磁盘
>> 后台线程会定时从LRU链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果在LRU链表中发现脏页,则把他们刷新到磁盘。—— 即: BUF_FLUSH_LRU
>> 控制块里会存储该缓冲页是否被修改的信息,所以在扫描LRU链表时,可以很轻松地获取到某个缓冲页是否脏页的信息。
3.4、LRU链表
>> 线性预读:如果顺序访问某个区(extent,一个区默认64个页)的页面超过了innodb_read_ahead_threshold(默认56)的值,就会触发一次异步读取下一个区中全部的页到Buffer Pool中的请求。
>> 随机预读:如果开启了随机预读功能(默认:innodb_random_read_ahead=OFF),如果某个区(extent)有13个连续的页面都已经被加载到了Buffer Pool中,无论这些页面是不是顺序读取的,都会触发一次异步读取本区全部的页到Buffer Pool中的请求。
针对LRU链表方案缺点的优化
(1)针对预读的优化
InnoDB规定,当磁盘上的某个页在初次加载(只是加载,没有涉及读取)到Buffer Pool中的某个缓冲页时,该缓冲页对应的控制块会被放到old区域的头部。这样预读页就只会在old区域,不会影响young区域中使用比较频繁的缓冲页。
(2)针对全表的优化
虽然首次加载放到的是old区域的头部,但是由于是全表扫描,会对加载的数据进行访问,那么第一次访问的时候,就会将该页放到young区域的头部。这样仍然会把哪些使用频率比较高的页面给“排挤”下去。
那怎么办呢?由于全表扫描有一个特点,就是它对某个页的频繁访问且总耗时很短。所以,针对这种情况,InnoDB规定,在对某个处于old区域的缓冲页进行第一次访问时,就在它对应的控制块中记录下这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内(即:innodb_old_blocks_time,默认为1000,单位为ms),那么该页面就不会从old区域移动到young区域的头部,否则将它移动到young区域的头部。
四、redo日志
4.1、什么是redo日志
如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交的事务在数据库中所作的更改也就丢失了。针对这种问题,怎么处理呢?
【方案1】在事务提交时,把该事务修改的所有页面都刷新到磁盘,刷新成功了才提示事务提交成功。
① 刷新一个完整的数据页太浪费了
虽然我们只修改了一条记录,但是会将这条记录所在的页(16kb)都刷新到磁盘上,会造成大量磁盘I/O的浪费。
② 随机I/O刷新起来比较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,并且该事务修改的这些页面可能并不相邻。这就意味着将某个事务修改额Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机I/O。而随机I/O要比顺序I/O慢,尤其是机械硬盘。
【方案2】在事务提交时,只需要把修改的内容记录一下就好了
例如:“将第0号表空间第100号页面中偏移量为1000处的值更新为2。”
4.2、redo简单日志类型
在对页面的修改是极其简单的情况下,redo日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值、具体修改后的内容是什么就好。比如,操作Max Row id。
=>> MLOG_1BYTE:表示在页面的某个偏移量处写入1字节的redo日志类型。
=>> MLOG_2BYTE:表示在页面的某个偏移量处写入2字节的redo日志类型。
=>> MLOG_4BYTE:表示在页面的某个偏移量处写入4字节的redo日志类型。
=>> MLOG_8BYTE:表示在页面的某个偏移量处写入8字节的redo日志类型。
=>> MLOG_WRITE_STRING:表示在页面的某个偏移量处写入了一个字节序列。
4.3、redo复杂日志类型 —— MLOG_COMP_REC_INSERT
4.4、redo日志组
在执行语句的过程中产生的redo日志,被InnoDB划分成了若干个不可分割的组。比如:更新Max Row ID属性时产生的redo日志为一组,是不可分割的;向聚簇索引/二级索引对应B+树的页面中插入一条记录时产生的redo日志为一组,是不可分割的等等。
InnoDB认为,比如向某个索引对应的B+树中插入一条记录的过程必须是原子的,不能说插入了一半之后就停止了。否则就会形成一棵不正确的B+树。所以他们规定在执行这些需要保证原子性的操作时,必须以组的形式来记录redo日志。在进行恢复时,针对某个组中的redo日志,要么把全部的日志都恢复,要么一条也不恢复。
4.5、MTR(Mini-Transaction)
对底层页面进行一次原子访问的过程被称为一个Mini-Transaction(MTR)。
事务、语句、MTR、redo日志之间的关系,如下图所示:
① 1个事务可以包含N条SQL语句
② 1条SQL语句可以包含N个MTR
③ 1条MTR可以包含N条redo日志
4.6、查看redo日志相关配置信息
-- 查看数据目录所在位置
show variables like 'datadir';
-- 查看redo日志文件所在的目录,默认为当前的数据目录
show variables like 'innodb_log_group_home_dir';
-- 查看每个redo日志文件的大小,默认值为48MB
show variables like 'innodb_log_file_size';
-- 查看redo日志文件的个数,默认值为2,最大值为100个
show variables like 'innodb_log_files_in_group';
五、undo日志
5.1、什么时undo日志
事务是需要保证原子性的,也就是说,事务中的操作要么全部完成,要么什么也不做。但有如下情况,会造成事务执行不完。
① 事务执行过程中可能遇到各种错误,比如:服务器宕机,操作系统异常,突然断电......
② 程序在事务执行过程中手动输入rollback语句结束当前事务的执行。
遇到上面的情况,为了保证事务的原子性,我们需要把数据还原回原来的样子,这个过程就叫做回滚(rollback)
数据库为了回滚而记录的日志,我们就称之为撤销日志(undo log)
注意一点,由于select操作并不会修改任何记录,所以并不需要记录相应的undo日志。
5.2、如何开启事务
开启的事务类型 | 解释 |
只读事务 | 通过对START TRANSACTION READ ONLY语句开启一个只读事务,在只读事务中,不可以对普通表进行增删改操作;但可以对临时表进行增删改操作 |
读写事务 | 通过对START TRANSACTION READ WRITE语句开启一个读写事务。使用BEGIN、START TRANSACTION语句开启的事务,默认也算是读写事务。在读写事务中可以对表执行增删改查操作。 |
5.3、何时分配事务id
只有在事务对表中的记录进行改动时才会为这个事务分配一个唯一的事务id,否则事务id默认为0。
(1)只读事务何时分配事务id
只有在它第一次对某个用户创建的临时表(CREATE TEMPORARY TABLE)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配的。
(2)读写事务何时分配事务id
只有在它第一次对某个表(包括用户创建的临时表)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配的。
5.4、事务id是怎么生成的
事务id本质上就是一个数字,事务id的生成策略如下:
① 内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把该变量值当作事务id分配给该事务,并且自增1
② 每当这个变量的值为256的倍数时,就会将该值刷新到系统表空间中页号为5的页面中一个名为Max Trx ID的属性中(占用8个字节)
③ 当系统下一次启动时,会将Max Trx ID的值加载到内存中,并加上256之后赋值给前面提到的全局变量。
为什么要加256?因为上次关机时,该全局变量的值可能大于磁盘页面中的Max Trx ID属性值。
5.5、事务id在记录中存储的位置
trx_id表示对该条记录进行改动的语句所对应的事务id
不同版本查询table_id的方式
【mysql 5.x】select * from information_schema.innodb_sys_tables;
【mysql 8.x】select * from information_schema.innodb_tables;
— 要养成终身学习的习惯 —
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南