MySQL - 进阶篇 - 6. InnoDB引擎


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_idroll_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)INSERTUPDATEDELETE 操作时产生,是一份便于数据回滚的日志

  • 对于 INSERT 操作:
    产生的Undo Log仅在回滚时使用,事务提交后可以立即删除,因为历史版本不需要了。

  • 对于 UPDATEDELETE 操作:
    产生的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:不仅回滚时使用,还用于快照读的历史版本(UPDATEDELETE 产生)。


✅ 为什么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_idsmin_trx_idmax_trx_id
  • m_ids:事务ID列表,记录了当前仍然“活跃”着的事务(尚未提交)。

  • min_trx_idm_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_IDDB_ROLL_PTR)、undo log版本链、ReadView三者一起协作,使得事务隔离既高效又稳健。

这一节内容其实是面试中高频考点,特别是RC vs RR的实现机制、MVCC原理,要非常熟悉 📝。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值