MySQL 二轮学习笔记·进阶篇·(八) InnoDB 引擎
InnoDB 引擎
1.逻辑存储结构
层级 | 大小 | 作用 |
---|---|---|
表空间(Tablespace) | 可变(GB 级) | 最高层逻辑容器,对应 .ibd 文件(独立表空间)或 ibdata1 (系统表空间) |
段(Segment) | 动态增长 | 每棵 B+ 树对应一个段。管理该树所有页的分配 |
区(Extent) | 1MB = 64 页 | 空间分配的基本单位。段从表空间中申请区 |
页(Page) | 16KB(默认) | I/O 的最小单位。B+ 树的节点(根、内节点、叶节点)都存储在页中 |
行(Row) | 可变 | 实际数据记录,存储在叶子页(聚簇索引)或索引页(二级索引) |
(1)表空间(Tablespace):数据的 “大仓库”
表空间是 InnoDB 存储的最高层级,负责整合所有数据文件,分为两种类型:
- 系统表空间(ibdata1):默认存储方式,所有数据库的表数据、索引、undo log、数据字典(表结构信息)都存放在这里。缺点是文件会无限膨胀,后期难以管理。
- 独立表空间(.ibd 文件):通过
innodb_file_per_table=ON
开启,每个表对应一个独立的.ibd
文件,仅存储当前表的数据和索引。优势是删除表时可直接释放空间,便于维护(生产环境必开)。
(2)段(Segment):表空间的 “功能分区”
段是表空间的子单位,按 “功能用途” 划分,核心分为三类:
- 数据段(Clustered Index Segment):即聚簇索引对应的段,InnoDB 中 “表即索引”,聚簇索引的叶子节点就是实际数据行,因此数据段直接存储表数据。
- 索引段(Secondary Index Segment):非聚簇索引(普通索引)对应的段,叶子节点存储的是 “聚簇索引键值”(而非实际数据),仅用于定位数据行。
- 回滚段(Rollback Segment):存储 undo log 的段,每个回滚段包含 1024 个 undo 页,用于事务回滚和 MVCC(后续重点讲)。
段类型 | 对应数据结构 | 是否 B+ 树? | 说明 |
---|---|---|---|
数据段(Clustered Index Segment) | 聚簇索引 | ✅ 是 | 表数据就存在这棵 B+ 树的叶子节点中 |
索引段(Secondary Index Segment) | 二级索引 | ✅ 是 | 每个二级索引是一棵独立的 B+ 树 |
回滚段(Rollback Segment) | Undo Log | ❌ 否 | 用于存储 undo 记录,结构是 链表/日志型,非树形 |
临时段(Temporary Segment) | 排序、临时表等 | ❌ 否 | 用于排序、GROUP BY 等操作的临时空间 |
系统段(System Segment) | 数据字典、事务系统等 | ❌ 否 | 存储元数据,如 TRX_SYS、SYS_TABLES 等 |
“段”是一个通用的空间管理抽象,B+ 树只是其中一种使用场景。
(3)区(Extent):段的 “最小分配单元”
区是段的组成单位,固定大小为 1MB(无论页大小是多少),每个区包含 64 个连续的 “页”(因为默认页大小 16KB,64×16KB=1MB)。
设计目的:避免 “频繁分配小空间”—— 如果直接按页分配,当表数据量增大时,会产生大量离散的页,导致磁盘 IO 碎片化;按区分配可保证 64 个页连续,减少随机 IO。
(4)页(Page):InnoDB 的 “最小 IO 单元”
页是 InnoDB 读写数据的基本单位(类比操作系统的 “块”),默认大小 16KB(可通过innodb_page_size
调整为 4K/8K/32K 等),核心页类型包括:
- 数据页(B-tree Node):存储表数据和索引的 B + 树节点(聚簇索引和非聚簇索引的叶子 / 非叶子节点)。
- undo 页(Undo Log Page):存储 undo log 记录。
- 重做日志页(Redo Log Page):存储 redo log 记录。
- 系统页(System Page):存储数据字典等系统信息。
(5)行(Row):数据的 “最小存储单元”
行是最终存储业务数据的单位,InnoDB 支持两种行格式(通过innodb_default_row_format
设置):
- Dynamic(默认):适合大字段(如 VARCHAR、TEXT),大字段会被拆分到 “溢出页” 存储,行中仅保留 20 字节的指针。
- Compact:大字段的前 768 字节存在行中,剩余部分存溢出页,兼容性更好但空间利用率略低。
此外,每行数据都会隐藏 3 个核心字段(MVCC 依赖):
DB_TRX_ID
(6 字节):最后修改该记录的事务 ID。DB_ROLL_PTR
(7 字节):指向该记录的上一个版本(undo log 中的位置)。DB_ROW_ID
(6 字节):默认不显示,当表没有主键且没有唯一索引时,InnoDB 会自动生成该字段作为聚簇索引键。
2.架构-内存与磁盘
InnoDB 的架构核心是 “内存缓冲 + 磁盘持久化”,通过内存减少磁盘 IO,通过磁盘保证数据不丢失。分为内存结构和磁盘结构两部分。
(1)内存结构
缓冲池(Buffer Pool)-直接读缓存
作用:缓存磁盘上的 “数据页” 和 “索引页”,后续查询若命中缓冲池,直接从内存读取,避免磁盘 IO(磁盘 IO 速度是内存的 10 万倍以上)。
- 缓存规则:采用 “LRU(最近最少使用)算法” 管理缓存,分为 “新生代(5/8)” 和 “老年代(3/8)”,新读取的页先放入新生代的 “midpoint” 位置,避免 “一次性全表扫描” 冲刷掉热点数据。
- 脏页(Dirty Page):缓冲池中被修改过,但尚未写入磁盘的数据页,后续会由后台线程异步刷新到磁盘(避免阻塞用户线程)。
更改缓冲区(Change Buffer)-优化非唯一索引写入速度
作用:当修改非唯一索引的记录时,若该索引页未在缓冲池中,InnoDB 不会直接去磁盘加载索引页,而是将修改操作缓存到 “更改缓冲区”,后续当该索引页被读取到缓冲池时,再合并修改(称为 “merge”)。
- 适用场景:非唯一索引(唯一索引需要校验唯一性,必须加载索引页,无法使用更改缓冲区)。
- 优势:减少写操作的磁盘 IO,尤其适合 “写多读少” 或 “批量插入” 场景(如订单表插入)。
自适应哈希索引(Adaptive Hash Index)-加速查找
作用:InnoDB 会自动分析 SQL 查询,对 “频繁命中的索引条件”(如
WHERE id=100
),在内存中构建哈希索引,将 B + 树的 “树查找”(O (log n))转化为 “哈希查找”(O (1)),提升查询速度。- 特点:完全自动,无需人工配置,仅缓存热点查询,占用内存小(不超过缓冲池的 10%)。
日志缓冲区(Log Buffer)-redo log缓冲区
作用:缓存事务产生的 redo log,避免每次生成 redo log 都写入磁盘(减少磁盘 IO)。
- 刷新规则:默认每 1 秒自动刷新到磁盘,或事务提交时(
innodb_flush_log_at_trx_commit=1
,生产环境必设,保证事务持久性),或当缓冲池占用超过 50% 时。
- 刷新规则:默认每 1 秒自动刷新到磁盘,或事务提交时(
(2)磁盘结构
数据文件(.ibd/.ibdata1)
即表空间对应的文件,存储表数据、索引、undo log(独立表空间中,undo log 默认存系统表空间,可通过
innodb_undo_tablespaces
拆分到独立文件)。重做日志文件(Redo Log File, ib_logfile0/ib_logfile1)
- 作用:记录 “数据页的物理修改”(如 “页 100 的偏移量 200 处的值从 10 改为 20”),用于系统崩溃后的恢复(保证事务的持久性)。
特点:
- 固定大小(通过
innodb_log_file_size
设置,生产环境建议 2-4GB),循环写入(写满后覆盖旧日志)。 - 双文件(ib_logfile0 和 ib_logfile1),交替写入,避免单文件损坏导致日志丢失。
- 固定大小(通过
回滚日志文件(Undo Log file)
- 作用:记录 “事务的逻辑反向操作”(如 “插入一条记录” 对应 “删除该记录”,“更新值从 10 到 20” 对应 “更新值从 20 到 10”),用于事务回滚和 MVCC。
- 特点:undo log 会被 “purge 线程” 异步回收(当事务提交且没有其他事务依赖该 undo log 时)。
系统表空间文件(ibdata1)
默认存储数据字典(表结构、列信息、索引信息)、undo log、临时表空间等,建议通过参数拆分(如
innodb_data_file_path
设置多个文件,innodb_undo_tablespaces
拆分 undo log)。
3.后台线程
InnoDB 通过多后台线程实现 “异步操作”,避免阻塞用户查询 / 更新线程,核心线程包括 4 类:
(1)Master Thread-核心线程
InnoDB 的核心线程,优先级最高,负责调度其他线程,主要工作:
- 每 1 秒:刷新 100 个脏页到磁盘;合并 100 个更改缓冲区;检查 redo log 缓冲是否需要刷新。
- 每 10 秒:刷新所有脏页到磁盘;合并所有更改缓冲区;回收无用的 undo log;检查表空间是否需要扩展。
(2)IO Thread-处理IO
负责处理磁盘 IO 的 “回调任务”,避免 Master Thread 阻塞在 IO 等待上,分为 4 类(可通过show engine innodb status
查看):
- read thread(4 个默认):处理数据页 / 索引页的读取请求。
- write thread(4 个默认):处理脏页、更改缓冲区、redo log 的写入请求。
- insert buffer thread(1 个):处理更改缓冲区的 merge 操作。
- log thread(1 个):处理 redo log 的刷新操作。
(3)Purge Thread-回收undo log
- 作用:异步回收 “已提交事务的 undo log”(当事务提交后,若没有其他事务依赖该 undo log,Purge Thread 会将其标记为可重用,释放空间)。
- 优势:早期版本 Purge 操作由 Master Thread 负责,会导致 Master Thread 繁忙;独立后提升了并发性能(默认 1 个,可通过
innodb_purge_threads
调整为多个)。
(4)Page Cleaner Thread-脏页刷新
- 作用:专门负责将缓冲池中的 “脏页” 刷新到磁盘,替代了早期 Master Thread 的脏页刷新工作。
- 优势:避免 Master Thread 因刷新脏页导致的 “用户线程阻塞”(如大量写操作导致脏页堆积时,Page Cleaner Thread 会逐步刷新,不影响用户查询)。
4.InnoDB 事务原理-日志机制
事务是数据库的核心能力,InnoDB 通过日志机制和锁机制保证事务的 ACID 特性(原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability)。
(1)Redo Log-保证持久性
如果没有 redo log,事务提交时需要将 “缓冲池中的脏页” 直接写入磁盘(即 “刷脏页”)。但脏页是 16KB 的整页,而事务可能只修改了页中的 1 个字节,直接刷整页会导致 “IO 浪费”;且刷脏页是随机 IO(数据页在磁盘上离散存储),速度慢。
redo log 的解决思路:记录 “修改的物理位置和内容”(如 “页 100 的偏移量 200,值从 10→20”),每条记录仅几十字节,且 redo log 是顺序写入(磁盘顺序 IO 速度远快于随机 IO)。
WAL 机制
WAL(Write-Ahead Logging):事务提交时,先写 redo log,再写缓冲池(脏页),最后由后台线程将脏页刷到磁盘。流程如下:
- 事务修改数据:先修改缓冲池中的数据页(生成脏页),同时生成 redo log,写入 “redo log 缓冲”。
- 事务提交:将 redo log 缓冲中的日志 “刷新到磁盘的 redo log 文件”(即 “写日志”)。
- 后台线程:异步将缓冲池中的脏页刷新到磁盘数据文件(即 “刷脏页”)。
即使步骤 3 未完成时系统崩溃,重启后 InnoDB 会通过 redo log 恢复未刷脏页的修改,保证事务持久性。
redo log 的关键参数
innodb_flush_log_at_trx_commit
控制事务提交时 redo log 的刷新策略(生产环境必设为 1):
1(默认推荐):事务提交时,redo log 直接刷到磁盘(保证持久性,最安全)。
0:事务提交时,redo log 仅写入操作系统缓存,不刷磁盘(性能高,但系统崩溃会丢失最后 1 秒的日志)。
2:事务提交时,redo log 写入操作系统缓存并刷到磁盘(但仅刷到磁盘缓存,若磁盘掉电仍会丢失)。
(2)Undo Log-保证原子性,MVCC的核心
事务回滚-原子性
undo log 记录事务的逻辑反向操作,当事务执行rollback时,InnoDB通过undo log撤销已执行的修改
例:事务执行UPDATE t SET age=20 WHERE id=1
(原 age=18),会生成 undo log:UPDATE t SET age=18 WHERE id=1
,回滚时,InnoDB 执行该 undo log,将数据恢复到修改前的状态。
支持MVCC多版本并发控制
当多个事务并发读写时,undo log会形成版本链,让不同事务看到数据的不同版本,避免读写冲突
undo log的声明周期
事务开始:执行修改操作时,生成undo log,写入undo log页
事务提交:undo log不会立即删除,而是标记为可回收(因为其他事务可能通过MVCC依赖此版本)
回收:Purge Thread异步检查,当没有事务依赖该undo log时,将其回收,释放空间
5.MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB实现高并发读写的核心机制,其目标是:读操作不阻塞写操作,写操作不阻塞读操作(即快照读),同时保证事务隔离性。
没有MVCC时,事务隔离依赖锁:读操作加共享锁(S锁),写操作加排他锁(X锁),S锁和X锁互斥,导致读阻塞写,写阻塞读,并发性能极低。
MVCC解决思路:为每条记录维护多个版本,读操作读取历史版本,写操作生成新版本。
(1)MVCC的三大组成部分
MVCC的实现依赖隐藏字段+undo log版本链+ReadView,三者协同工作:
隐藏字段
每条记录的3个隐藏字段是版本管理的基础:
DB_TRX_ID,标记最后修改该记录的事务ID,事务开始时会分配全局唯一ID
DB_ROLL_PTR,指向该记录的上一个版本,即对应的undo log记录,形成版本链
DB_ROW_ID,仅当无主键/唯一索引时使用,不影响版本管理
undo log 版本链
每次对记录进行修改时,InnoDB会生成一条undo log记录,存储修改前的旧版本数据,将当前记录的DB_ROLL_PTR指向这条undo log,形成链表,更新当前记录的DB_TRX_ID为当前事务ID
ReadView 读视图-事务的版本可见性规则
读视图是事务在执行快照读时形成的一致性规则,本质是一致性规则,用于判断版本链中哪些版本对当前事务可见
ReadView包含4个核心参数:
m_ids:生成ReadView时,当前所有活跃的事务ID列表,即已开始但未提交的事务
min_trx_id:m_ids中最小的事务id,即当前活跃事务的最小id
max_trx_id:生成ReadView时,下一个要分配的事务ID,注意不是m_ids的最小id而是全局事务id的下一个值
creator_trx_id:生成该ReadView的当前事务ID
(2)ReadView:事务的版本可见性规则
对于版本链中的某一版本,其DB_TRX_ID记为trx_id
- 若
trx_id == creator_trx_id
:该版本是当前事务自己修改的,可见。 - 若
trx_id < min_trx_id
:该版本由 “已提交的事务” 生成(因为事务 ID 递增,小于最小活跃 ID 说明事务已结束),可见。 - 若
trx_id >= max_trx_id
:该版本由 “生成 ReadView 后才开始的事务” 生成(事务 ID 超过下一个分配 ID,说明事务是新启动的),不可见。 若min_trx_id <= trx_id < max_trx_id:需判断trx_id是否在m_ids中:
- 若在:事务仍活跃(未提交),版本不可见;
- 若不在:事务已提交,版本可见。
若当前版本不可见,则通过DB_ROLL_PTR
追溯上一个版本,重复上述判断,直到找到可见版本或版本链结束(此时返回空结果)。
条件 | 是否可见 | 说明 |
---|---|---|
trx_id < min_trx_id | ✅ 可见 | 事务已结束(提交) |
trx_id >= max_trx_id | ❌ 不可见 | 未来事务(ReadView 创建后才启动) |
min_trx_id <= trx_id < max_trx_id | 看是否在 m_ids 中: - 在 → ❌ 不可见 - 不在 → ✅ 可见 | 在:活跃未提交;不在:已提交 |
(3)MVCC在不同隔离级别下的差异
InnoDB 的事务隔离级别中,读已提交(RC) 和可重复读(RR) 均基于 MVCC 实现 “快照读”,核心差异在于ReadView 的生成时机不同,导致 “重复读” 的效果不同。
读已提交(RC):每次快照读生成新的 ReadView
- 规则:事务中每次执行普通
SELECT
(快照读)时,都会重新生成一个 ReadView。 - 效果:同一事务中多次查询,可能看到不同的结果(因为每次 ReadView 不同,能看到 “两次查询之间提交的事务” 的修改)。
可重复读(RR):仅第一次快照读生成 ReadView
- 规则:事务中第一次执行普通
SELECT
(快照读)时,生成一个 ReadView,后续所有快照读都复用这个 ReadView。 - 效果:同一事务中多次查询,结果始终一致(因为 ReadView 不变,即使其他事务提交,也看不到其修改),实现 “可重复读”。
快照读与当前读的区别
需要注意的是,MVCC 仅针对 “快照读” 生效,而 “当前读” 仍需通过锁机制保证隔离性:
- 快照读:普通
SELECT
(不加锁),基于 MVCC 读取历史版本,不阻塞写操作,也不被写操作阻塞。 当前读:读取 “最新版本” 的操作,会加锁,阻塞写操作,也会被写操作阻塞,包括:
SELECT ... FOR UPDATE
(加排他锁)SELECT ... LOCK IN SHARE MODE
(加共享锁)INSERT
/UPDATE
/DELETE
(默认加排他锁)
小结
InnoDB 的设计是一个 “环环相扣” 的整体,我们可以用一条 “写数据” 的流程串联所有核心机制:
- 用户执行 UPDATE:事务开始,分配事务 ID(如 trx_id=5)。
内存操作:
- 从磁盘读取目标数据页到 “缓冲池”;
- 修改缓冲池中的数据(生成脏页),同时生成 “undo log”(记录旧版本,用于回滚和 MVCC)和 “redo log”(记录物理修改,用于持久化);
- undo log 加入版本链,数据页的
DB_TRX_ID
更新为 5,DB_ROLL_PTR
指向新生成的 undo log; - redo log 写入 “redo log 缓冲”。
事务提交:
- 触发 “WAL 机制”:将 redo log 缓冲中的日志刷到磁盘的 “redo log 文件”(保证持久性);
- 事务标记为提交,undo log 标记为 “可回收”。
后台线程工作:
Page Cleaner Thread
异步将缓冲池中的脏页刷到磁盘的 “.ibd 文件”;Purge Thread
异步回收无事务依赖的 undo log;Master Thread
每 10 秒检查脏页和 undo log,确保磁盘数据与内存一致。
其他事务查询:
- 若执行普通
SELECT
(快照读),生成 ReadView,通过版本链和可见性规则读取历史版本(MVCC); - 若执行
SELECT ... FOR UPDATE
(当前读),加排他锁,读取最新版本,阻塞其他写操作。
- 若执行普通
通过这套机制,InnoDB 同时实现了事务安全(ACID)、高并发(MVCC)、崩溃恢复(redo log) 三大核心目标,成为 MySQL 生产环境的首选存储引擎。
本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据