MySQL - 进阶篇 - 6. InnoDB引擎
6.1 逻辑存储结构
InnoDB的逻辑存储结构如下图所示:
1️⃣ 表空间
表空间是InnoDB存储引擎逻辑结构的最高层。如果启用了参数 innodb_file_per_table
(在MySQL 8.0中默认开启),则每张表都会有一个独立的表空间(xxx.ibd文件)。一个MySQL实例可以对应多个表空间,用于存储记录、索引等数据。
2️⃣ 段
段分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment)。InnoDB是索引组织表,数据段是B+树的叶子节点,索引段是B+树的非叶子节点。段用于管理多个区(Extent)。
3️⃣ 区
区是表空间的单元结构,每个区大小为1M。默认情况下InnoDB存储引擎页大小为16KB,因此一个区包含64个连续的页。
4️⃣ 页
页是InnoDB磁盘管理的最小单元,每个页的大小默认为16KB。为了保证页的连续性,InnoDB每次从磁盘申请4-5个区。
5️⃣ 行
行是InnoDB存储引擎的数据存放单位。
InnoDB行数据中默认包含两个隐藏字段:
-
Trx_id
:每次对记录进行改动时,会把事务ID赋值给trx_id
隐藏列。 -
Roll_pointer
:每次对记录进行改动时,会将旧版本写入undo日志,这个隐藏列指向修改前的记录。
🧠 理论理解
InnoDB将数据从宏到微分为5层结构:表空间 > 段 > 区 > 页 > 行。
-
表空间(Tablespace):是最高层结构,MySQL 8.0默认每张表独立一个表空间,支持灵活管理。
-
段(Segment):InnoDB的B+树中,叶子节点是数据段,非叶子是索引段。段是“管理区的容器”。
-
区(Extent):每个区是64个连续的页,大小1MB,是空间申请和释放的基本单元。
-
页(Page):InnoDB的最小磁盘管理单位,默认16KB,数据/索引/Undo等都基于页存储。
-
行(Row):数据存储的最小单位,支持事务字段如
trx_id
和roll_pointer
实现版本管理。
🏢 企业实战理解
-
阿里巴巴/天猫:大表优化时会监控页利用率和区分布,避免过度碎片影响性能。
-
字节跳动:热表大索引多时优化页合并,避免随机I/O过高。
-
Google Cloud SQL:区和页的预分配策略被调整,用于应对突发大数据插入。
6.2 架构
6.2.1 概述
MySQL 5.5开始默认使用InnoDB存储引擎,它擅长事务处理,具备崩溃恢复特性,是日常开发中最常用的存储引擎。InnoDB架构分为内存结构和磁盘结构。
InnoDB的后台线程包括:
-
Master Thread:核心调度线程,负责刷新缓冲池、合并插入缓存、回收undo页等。
-
IO Thread:异步IO请求的回调处理。
-
Purge Thread:回收已提交事务的undo log。
-
Page Cleaner Thread:协助刷新脏页,减轻Master Thread压力。
🧠 理论理解
InnoDB架构分为 内存结构 和 磁盘结构。
内存结构
-
Buffer Pool:缓存数据页和索引页,命中率影响查询性能,建议占物理内存70~80%。
-
Change Buffer:缓冲二级索引的变更,降低磁盘I/O。
-
Adaptive Hash Index:对频繁访问页自动建立hash索引。
-
Log Buffer:事务提交时先写到这里,再刷盘。
磁盘结构
-
System Tablespace:保存字典/Undo等元数据。
-
File-Per-Table Tablespaces:每个表对应
.ibd
文件。 -
Redo Log / Undo Log:持久化与回滚机制。
-
双写缓冲区:保证崩溃恢复时一致性。
🏢 企业实战理解
-
美团:Buffer Pool策略动态调优,在高峰期自动扩容。
-
阿里云:自研文件系统加速双写缓冲的性能。
-
OpenAI:模型训练中批量写入时优化Redo刷盘策略。
6.2.2 内存结构
InnoDB的内存结构包括四大块:
1️⃣ Buffer Pool
缓冲池缓存了索引页、数据页、undo页、插入缓存、自适应哈希索引、锁信息等。以页(Page)为单位管理,分为free page、clean page、dirty page三类。缓冲池的大小通过 innodb_buffer_pool_size
参数设置。
2️⃣ Change Buffer
更改缓冲区,用于缓存非唯一二级索引页的变更。当执行DML语句时,如果数据页不在Buffer Pool中,不会立即操作磁盘,而是先将变更记录到Change Buffer中,等数据页加载到Buffer Pool时再合并。
3️⃣ Adaptive Hash Index
自适应哈希索引是InnoDB监控索引页访问模式时自动建立的Hash索引,用于优化等值查询。参数:adaptive_hash_index
。
4️⃣ Log Buffer
日志缓冲区用于暂存Redo Log和Undo Log数据,默认大小16MB,通过参数 innodb_log_buffer_size
设置。
6.2.3 磁盘结构
1️⃣ System Tablespace
系统表空间存储更改缓冲区、数据字典等内容,默认文件名 ibdata1
。
2️⃣ File-Per-Table Tablespaces
当 innodb_file_per_table
开启时,每张表对应一个单独的.ibd文件。
3️⃣ General Tablespaces
通过 CREATE TABLESPACE
创建,允许多个表共享一个通用表空间。
4️⃣ Undo Tablespaces
用于存储undo日志,MySQL实例初始化时自动创建两个默认undo表空间。
5️⃣ Temporary Tablespaces
用于存储会话级和全局级的临时表。
6️⃣ Doublewrite Buffer Files
双写缓冲区用于在刷新数据页到磁盘前先写入双写缓冲区,保证数据安全。
7️⃣ Redo Log
重做日志实现事务持久性,分为日志缓冲(内存)和重做日志文件(磁盘)。
6.2.4 后台线程
🧠 理论理解
-
Master Thread:协调刷新/合并/回收任务。
-
IO Thread:专门处理磁盘I/O。
-
Purge Thread:清理已经提交事务的Undo Log。
-
Page Cleaner Thread:异步刷脏页到磁盘。
🏢 企业实战理解
-
字节跳动:自研了IO调度器优化Purge性能。
-
腾讯:监控Page Cleaner滞后,防止脏页爆表。
6.3 事务原理
🧠 理论理解
ACID四大特性中:
-
原子性/一致性/持久性:由Redo + Undo保证;
-
隔离性:由MVCC + 锁机制保证。
事务提交时:先写Redo,再刷脏页,最后清理Undo。
🏢 企业实战理解
-
美团:通过Redo性能调优,降低交易系统延迟。
-
阿里:设计“分段提交”策略避免Redo日志阻塞。
6.3.1 事务基础
事务是一个不可分割的工作单元,满足ACID四大特性:
-
原子性(Atomicity)
-
一致性(Consistency)
-
隔离性(Isolation)
-
持久性(Durability)
InnoDB利用Redo Log、Undo Log、MVCC等机制实现事务四大特性。
6.3.2 Redo Log
Redo Log用于记录事务提交时数据页的物理修改,确保事务持久性。采用WAL(Write-Ahead Logging)机制,通过顺序写提高磁盘性能。
6.3.3 Undo Log
Undo Log用于记录数据修改前的信息,支持回滚(事务原子性)和MVCC(多版本并发控制)。
6.4 MVCC
6.4.1 基本概念
-
当前读:加锁读取,读取最新数据。
-
快照读:读取数据的历史版本,不加锁。
6.4.2 隐藏字段
InnoDB为每条记录添加隐藏字段:
-
DB_TRX_ID
:记录最近修改事务ID。 -
DB_ROLL_PTR
:回滚指针,指向旧版本。 -
DB_ROW_ID
:隐藏主键(仅在无主键表中添加)。
6.4.3 Undo Log
6.4.3.1 介绍
回滚日志(Undo Log)在 INSERT
、UPDATE
、DELETE
操作时产生,是一份便于数据回滚的日志。
-
对于
INSERT
操作:
产生的Undo Log仅在回滚时使用,事务提交后可以立即删除,因为历史版本不需要了。 -
对于
UPDATE
、DELETE
操作:
产生的Undo Log不仅在回滚时使用,同时还在快照读(MVCC)时使用,用于生成历史版本,因此即使事务提交了也不会立即删除,必须等到所有活跃事务用完之后才可以清理。
6.4.3.2 版本链
来看一个例子:
假设有一张表,原始数据为:
字段 | 含义 |
---|---|
DB_TRX_ID | 代表最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID(自增) |
DB_ROLL_PTR | 回滚指针,指向上一个旧版本的Undo Log;因为这是刚插入的数据,没有修改过,所以该值为null |
接下来有四个并发事务同时访问并修改这条记录。
A. 第一步
事务2执行了第一次修改操作,它会:
-
在Undo Log中记录“数据变更之前”的信息(即老数据的快照);
-
更新当前记录的:
-
DB_TRX_ID
(标记为事务2的ID) -
DB_ROLL_PTR
(指向刚刚生成的Undo Log)
-
此时形成了版本链的第一层。
B. 第二步
事务3执行修改:
-
同样记录Undo Log,保存修改前的数据;
-
更新
DB_TRX_ID
(事务3的ID); -
更新
DB_ROLL_PTR
(指向新的Undo Log)。
现在版本链长度为2,链表结构如下:
事务3修改的版本
→ 事务2修改的版本
→ 原始数据
C. 第三步
事务4也进行了修改:
-
写入Undo Log;
-
更新
DB_TRX_ID
(事务4的ID); -
更新
DB_ROLL_PTR
(指向新Undo Log)。
最终形成的版本链:
事务4修改的版本
→ 事务3修改的版本
→ 事务2修改的版本
→ 原始数据
🔍 版本链总结
-
链表头部:最新的旧版本(最近的一次修改前的状态);
-
链表尾部:最早的旧版本(原始数据);
-
当有多个事务同时对一条记录进行修改时,会形成多层Undo Log链,用于实现回滚或快照读的版本回溯。
🛠️ 补充细节(新增)
✅ Undo Log的内部结构
InnoDB中Undo Log是以**段(Segment)**的形式进行管理的,它存储在前面提到的Rollback Segment
里,每个Segment最多可包含1024个Undo Log记录。
Undo Log文件主要包含两类记录:
-
Insert Undo Log:仅在事务回滚时使用(
INSERT
操作产生); -
Update Undo Log:不仅回滚时使用,还用于快照读的历史版本(
UPDATE
、DELETE
产生)。
✅ 为什么Insert的Undo Log能立即删除?
因为:
-
INSERT
不涉及旧数据(它是新插入的); -
回滚时只需删除新插入的行即可,不存在历史版本依赖;
-
所以一旦事务提交,这类Undo Log就没用了,可以立即回收。
而 UPDATE
/ DELETE
涉及修改历史,其他事务的快照读还可能用到,不能马上删除。
✅ Undo Log的作用范围
Undo Log不仅用于:
-
回滚操作(保证原子性);
还用于:
-
快照读(实现MVCC),使得其他并发事务可以“看到”符合隔离级别的数据版本。
这也是为什么Undo Log是InnoDB中实现事务隔离性和原子性的核心机制。
🏢 企业实战场景(新增)
-
阿里巴巴:商品库存管理中,快速下单导致大量并发
UPDATE
操作,通过Undo Log保证数据一致性与快速回滚。 -
字节跳动:视频审核系统的状态流转,每次状态更新会生成Undo Log,保证即使出现异常也能回滚恢复。
-
OpenAI:在API调用额度记录更新时,如果中间失败,可通过Undo Log进行补偿回滚,避免计费异常。
🚀 性能提示(新增)
-
Undo Log膨胀问题:如果事务过大、长时间不提交,会导致Undo Log堆积,影响性能甚至阻塞Purging线程。
-
优化建议:
-
大事务拆小;
-
控制事务持有时间;
-
定期监控
SHOW ENGINE INNODB STATUS
检查Undo Log使用情况。
-
6.4.4 ReadView
📊 字段说明
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 当前活跃事务中最小的事务ID |
max_trx_id | 当前活跃事务中最大事务ID + 1(因为事务ID是自增的,预分配下一ID) |
creator_trx_id | 创建这个ReadView的事务ID |
🛠️ 访问规则
条件 | 是否可以访问该版本 | 说明 |
---|---|---|
trx_id == creator_trx_id | ✅ 可以 | 数据是当前事务修改的版本 |
trx_id < min_trx_id | ✅ 可以 | 数据对应的事务已经提交 |
trx_id > max_trx_id | ❌ 不可以 | 数据对应的事务是在ReadView生成后新开的事务 |
min_trx_id ≤ trx_id ≤ max_trx_id 且不在m_ids 中 | ✅ 可以 | 数据对应的事务已经提交(虽然事务ID范围在活跃区间,但其实已提交) |
📝 ReadView概述
ReadView(读视图)是InnoDB实现MVCC机制的关键组件,它在快照读执行时生成,记录并维护当前系统活跃的事务信息。
它的主要作用是:
➡️ 决定数据版本是否“对当前事务可见”。
➡️ 在快照读中,通过比对**版本链(Undo Log链表)**的事务ID和ReadView的事务ID集合,判断是否应该返回该版本的数据。
注意:
-
trx_id
表示版本链中每个数据版本对应的事务ID; -
不同的隔离级别,ReadView的生成时机不同:
-
READ COMMITTED
:事务中每次快照读都会生成一个新的ReadView; -
REPEATABLE READ
:事务中第一次快照读生成ReadView,后续的快照读复用该ReadView。
-
🔍 详细扩展补充(新增)
✅ 为什么有 m_ids
、min_trx_id
、max_trx_id
?
-
m_ids
:事务ID列表,记录了当前仍然“活跃”着的事务(尚未提交)。 -
min_trx_id
:m_ids
中的最小事务ID。任何trx_id < min_trx_id
的数据版本,一定是“已经提交”的。 -
max_trx_id
:新事务的预分配ID,保证了即使新事务还没插入m_ids
,也不会误判。 -
creator_trx_id
:标识是哪个事务创建了这个ReadView,用于允许自己读到自己未提交的修改(当前读)。
这些参数共同保证了:
1️⃣ 不可见的数据(未提交)被正确“隔离”掉;
2️⃣ 可见的数据(自己或已提交的)被正确返回。
✅ 图解版本链访问过程(简述)
当执行快照读时,InnoDB会:
1️⃣ 从数据页读取记录;
2️⃣ 检查其 trx_id
:
-
是不是自己(
creator_trx_id
)?✅ 直接可见 -
比
min_trx_id
小?✅ 说明早就提交了,可见 -
比
max_trx_id
大?❌ 是新事务产生的,绝对不可见 -
在活跃范围内(
min_trx_id ≤ trx_id ≤ max_trx_id
)?
➔ 如果不在m_ids
中:✅ 已提交
➔ 如果在m_ids
中:❌ 未提交
这种流程反复在Undo Log版本链中迭代,直到匹配到可见版本。
✅ ReadView在不同隔离级别中的作用
-
READ COMMITTED:每次执行快照读时生成新的ReadView,确保读取到最新已提交数据。
-
REPEATABLE READ:只有第一次快照读时生成ReadView,后续所有快照读都复用它,实现可重复读。
这也是为什么RR隔离级别下,事务中多次读取相同记录时内容不变,而RC隔离级别下内容可能变化。
✅ 补充场景举例
-
字节跳动:内容平台需要处理多条评论或视频元数据的读取,为避免“数据闪烁”,默认采用RR隔离级别,复用同一个ReadView。
-
美团:下单时库存读取采用RC隔离级别,每次读取获取最新库存状态,防止超卖。
-
OpenAI:API账单系统在生成对账单时需要保持一致快照,复用ReadView;但在实时监控中使用RC保证数据实时性。
🚀 性能提示(新增)
-
ReadView的开销:虽然ReadView是轻量对象,但在高并发场景下,频繁生成ReadView(尤其是RC隔离级别)会带来小幅性能损耗。
-
索引优化:大厂常配合“聚集索引+版本链+ReadView”设计,确保快照读时只需最少遍历。
6.4.5 原理分析
6.4.5.1 RC隔离级别
RC隔离级别下,在事务中每一次执行快照读时都会生成一个新的ReadView。
我们来分析一个案例:事务5中,两次快照读读取 id = 30
的记录,是如何获取数据的。
在事务5中,查询了两次 id = 30
的记录,由于隔离级别是 Read Committed,所以每一次快照读都会生成一个新的ReadView。两次生成的ReadView如下:
第一次快照读
在读取数据时,需要根据生成的ReadView以及版本链访问规则,到undo log版本链中匹配数据,来决定这次快照读返回哪条数据。
匹配过程如下(从undo log的版本链顶端开始匹配):
1️⃣ 匹配第一条记录,它的 trx_id = 4
:
-
① 不满足
-
② 不满足
-
③ 不满足
-
④ 不满足
未命中,继续下一条。
2️⃣ 匹配第二条记录,它的 trx_id = 3
:
-
① 不满足
-
② 不满足
-
③ 不满足
-
④ 不满足
未命中,继续下一条。
3️⃣ 匹配第三条记录,它的 trx_id = 2
:
-
① 不满足
-
② 满足 ✅(匹配命中)
终止匹配,此次快照读返回的是这条记录的数据。
第二次快照读
匹配过程与第一次类似(从undo log的版本链顶端开始匹配):
1️⃣ 匹配第一条记录,它的 trx_id = 4
:
-
① 不满足
-
② 不满足
-
③ 不满足
-
④ 不满足
继续下一条。
2️⃣ 匹配第二条记录,它的 trx_id = 3
:
-
① 不满足
-
② 满足 ✅
终止匹配,此次快照读返回的是这条记录的数据。
-
RC场景:每次读取都是新快照,适合实时性要求高的业务。
-
RR场景:复用同一快照,保证事务内一致性。
🏢 企业实战理解
-
字节跳动:对资产数据、用户隐私数据等严格采用RR;
-
阿里云数据库:为金融业务实现“可选隔离级别”,满足不同场景下的强一致读/高性能读切换。
6.4.5.3 RR隔离级别
RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续操作会复用该ReadView。
RR隔离级别是可重复读,即在同一个事务中,执行两次相同的 SELECT
语句,查询到的结果是完全一致的。
MySQL是如何做到这一点的呢?
原因就在于:
-
在RR隔离级别下,只在第一次快照读时生成ReadView;
-
后续快照读都复用同一个ReadView;
-
因为ReadView一致,所以版本链匹配逻辑也一致,快照读返回的结果自然一致。
因此,实现了可重复读。
📈 我的补充(增强理解)
✅ 为什么要每次快照读都生成新的ReadView(RC)?
RC(Read Committed)的目标是:只要事务已提交,其他事务立刻可见。
所以它必须保证:
-
当前事务读取的都是“已提交版本”;
-
这就是为什么RC在每次快照读时都重新生成ReadView,确保看到的数据是“最新提交”。
这种策略的优点是:读取到的数据实时性强;
缺点是:同一个事务内,数据不一定重复读到同样的结果(不可重复读)。
✅ 为什么RR只生成一次ReadView?
RR(Repeatable Read)强调:同一事务内的多次读取结果必须一致。
所以它采用的策略是:
-
只在事务的第一次快照读时生成ReadView;
-
后续所有快照读复用这个ReadView。
这样即使其他事务提交了新数据,本事务也不会“看到”,保证了数据的可重复性。
✅ 小提示:版本链匹配规则补充
ReadView对版本链的访问规则如下:
-
trx_id == creator_trx_id → 访问
-
trx_id < min_trx_id → 访问
-
trx_id > max_trx_id → 不访问
-
min_trx_id ≤ trx_id ≤ max_trx_id 且不在m_ids中 → 访问
这套规则是MVCC实现的核心机制,用于判断数据是否“可见”。
✅ 企业实战场景提示
-
字节跳动:在抖音、今日头条等场景中,采用默认RR隔离级别,保障同一次请求流转中多次读取数据一致,防止“数据闪烁”。
-
阿里巴巴:电商大促时,对于库存数据读取,采用RC隔离级别,保证读取最新库存,防止超卖。
-
OpenAI:API调用日志处理采用RR隔离级别,保证事务范围内数据快照一致,减少数据偏差风险。
-
Google Cloud Spanner:为实现强一致事务,其实现上采用更复杂的MVCC和时间戳分布式锁机制,但基本理念类似。
✅ 结尾补充
总结来说:
-
RC隔离级别 → 实时性高,但不保证可重复读;
-
RR隔离级别 → 数据一致性高,实现可重复读。
InnoDB的MVCC机制结合隐藏字段(DB_TRX_ID
、DB_ROLL_PTR
)、undo log版本链、ReadView三者一起协作,使得事务隔离既高效又稳健。
这一节内容其实是面试中高频考点,特别是RC vs RR的实现机制、MVCC原理,要非常熟悉 📝。