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)内存结构
  1. 缓冲池(Buffer Pool)-直接读缓存

    作用:缓存磁盘上的 “数据页” 和 “索引页”,后续查询若命中缓冲池,直接从内存读取,避免磁盘 IO(磁盘 IO 速度是内存的 10 万倍以上)。

    • 缓存规则:采用 “LRU(最近最少使用)算法” 管理缓存,分为 “新生代(5/8)” 和 “老年代(3/8)”,新读取的页先放入新生代的 “midpoint” 位置,避免 “一次性全表扫描” 冲刷掉热点数据。
    • 脏页(Dirty Page):缓冲池中被修改过,但尚未写入磁盘的数据页,后续会由后台线程异步刷新到磁盘(避免阻塞用户线程)。
  2. 更改缓冲区(Change Buffer)-优化非唯一索引写入速度

    作用:当修改非唯一索引的记录时,若该索引页未在缓冲池中,InnoDB 不会直接去磁盘加载索引页,而是将修改操作缓存到 “更改缓冲区”,后续当该索引页被读取到缓冲池时,再合并修改(称为 “merge”)。

    • 适用场景:非唯一索引(唯一索引需要校验唯一性,必须加载索引页,无法使用更改缓冲区)。
    • 优势:减少写操作的磁盘 IO,尤其适合 “写多读少” 或 “批量插入” 场景(如订单表插入)。
  3. 自适应哈希索引(Adaptive Hash Index)-加速查找

    作用:InnoDB 会自动分析 SQL 查询,对 “频繁命中的索引条件”(如WHERE id=100),在内存中构建哈希索引,将 B + 树的 “树查找”(O (log n))转化为 “哈希查找”(O (1)),提升查询速度。

    • 特点:完全自动,无需人工配置,仅缓存热点查询,占用内存小(不超过缓冲池的 10%)。
  4. 日志缓冲区(Log Buffer)-redo log缓冲区

    作用:缓存事务产生的 redo log,避免每次生成 redo log 都写入磁盘(减少磁盘 IO)。

    • 刷新规则:默认每 1 秒自动刷新到磁盘,或事务提交时(innodb_flush_log_at_trx_commit=1,生产环境必设,保证事务持久性),或当缓冲池占用超过 50% 时。
(2)磁盘结构
  1. 数据文件(.ibd/.ibdata1)

    即表空间对应的文件,存储表数据、索引、undo log(独立表空间中,undo log 默认存系统表空间,可通过innodb_undo_tablespaces拆分到独立文件)。

  2. 重做日志文件(Redo Log File, ib_logfile0/ib_logfile1)

    • 作用:记录 “数据页的物理修改”(如 “页 100 的偏移量 200 处的值从 10 改为 20”),用于系统崩溃后的恢复(保证事务的持久性)。
    • 特点:

      • 固定大小(通过innodb_log_file_size设置,生产环境建议 2-4GB),循环写入(写满后覆盖旧日志)。
      • 双文件(ib_logfile0 和 ib_logfile1),交替写入,避免单文件损坏导致日志丢失。
  3. 回滚日志文件(Undo Log file)

    • 作用:记录 “事务的逻辑反向操作”(如 “插入一条记录” 对应 “删除该记录”,“更新值从 10 到 20” 对应 “更新值从 20 到 10”),用于事务回滚和 MVCC。
    • 特点:undo log 会被 “purge 线程” 异步回收(当事务提交且没有其他事务依赖该 undo log 时)。
  4. 系统表空间文件(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,再写缓冲池(脏页),最后由后台线程将脏页刷到磁盘。流程如下:

  1. 事务修改数据:先修改缓冲池中的数据页(生成脏页),同时生成 redo log,写入 “redo log 缓冲”。
  2. 事务提交:将 redo log 缓冲中的日志 “刷新到磁盘的 redo log 文件”(即 “写日志”)。
  3. 后台线程:异步将缓冲池中的脏页刷新到磁盘数据文件(即 “刷脏页”)。

即使步骤 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

  1. trx_id == creator_trx_id:该版本是当前事务自己修改的,可见。
  2. trx_id < min_trx_id:该版本由 “已提交的事务” 生成(因为事务 ID 递增,小于最小活跃 ID 说明事务已结束),可见。
  3. trx_id >= max_trx_id:该版本由 “生成 ReadView 后才开始的事务” 生成(事务 ID 超过下一个分配 ID,说明事务是新启动的),不可见。
  4. 若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 的设计是一个 “环环相扣” 的整体,我们可以用一条 “写数据” 的流程串联所有核心机制:

  1. 用户执行 UPDATE:事务开始,分配事务 ID(如 trx_id=5)。
  2. 内存操作:

    • 从磁盘读取目标数据页到 “缓冲池”;
    • 修改缓冲池中的数据(生成脏页),同时生成 “undo log”(记录旧版本,用于回滚和 MVCC)和 “redo log”(记录物理修改,用于持久化);
    • undo log 加入版本链,数据页的DB_TRX_ID更新为 5,DB_ROLL_PTR指向新生成的 undo log;
    • redo log 写入 “redo log 缓冲”。
  3. 事务提交:

    • 触发 “WAL 机制”:将 redo log 缓冲中的日志刷到磁盘的 “redo log 文件”(保证持久性);
    • 事务标记为提交,undo log 标记为 “可回收”。
  4. 后台线程工作:

    • Page Cleaner Thread异步将缓冲池中的脏页刷到磁盘的 “.ibd 文件”;
    • Purge Thread异步回收无事务依赖的 undo log;
    • Master Thread每 10 秒检查脏页和 undo log,确保磁盘数据与内存一致。
  5. 其他事务查询:

    • 若执行普通SELECT(快照读),生成 ReadView,通过版本链和可见性规则读取历史版本(MVCC);
    • 若执行SELECT ... FOR UPDATE(当前读),加排他锁,读取最新版本,阻塞其他写操作。

通过这套机制,InnoDB 同时实现了事务安全(ACID)、高并发(MVCC)、崩溃恢复(redo log) 三大核心目标,成为 MySQL 生产环境的首选存储引擎。

分类: Java-Backend 标签: MySQL

评论

暂无评论数据

暂无评论数据

目录