存储引擎 InnoDB
目录
InnoDB 架构基础
内存结构
磁盘结构
数据存储与索引机制
InnoDB 的行格式
B+Tree 索引结构
索引性能优化
事务处理与 ACID
多版本并发控制(MVCC)
InnoDB 架构基础
InnoDB 的架构包括内存结构和磁盘结构两大部分。
内存结构
InnoDB 的内存结构是提升数据库性能的关键,通过合理的内存管理减少磁盘 I/O 操作,大幅提升数据访问速度。
InnoDB 的内存结构主要包括以下几个核心组件:
1. 缓冲池(Buffer Pool)
InnoDB 中最重要的内存组件,缓存了数据页、索引页、Undo 页、插入缓冲(Change Buffer)、自适应哈希索引页等
通过缓存减少磁盘 I/O 提升性能
页分为:
脏页:内存中的页已修改但尚未写入磁盘
干净页:内存中的页与磁盘数据一致
脏页会在 Checkpoint 或 LRU 淘汰时由后台线程刷盘
管理机制:
LRU 算法:缓冲池使用改进的 LRU(最近最少使用)算法管理页的淘汰,将频繁访问的页保留在内存中
预读机制:根据访问模式自动预读可能需要的页到缓冲池,减少未来的 I/O 操作
Checkpoint 机制:定期将脏页批量写入磁盘,保证内存和磁盘数据的一致性,同时减少崩溃恢复时间
2. 日志缓冲(Log Buffer)
日志缓冲用于暂存事务生成的 Redo 日志,减少 Redo 日志的磁盘 I/O 次数
在事务提交(COMMIT)或定时触发时将数据写入磁盘上的 redo log 文件
3. 插入缓冲(Change Buffer)
当插入非唯一二级索引时,若索引页不在缓冲池中,InnoDB 不会立即去磁盘加载索引页,而是先将变更记录到 Change Buffer 中,待后续读取该索引页或系统空闲时,再将 Change Buffer 中的变更合并到实际索引页,将随机 I/O 转化为批量的顺序 I/O
4. 自适应哈希索引(Adaptive Hash Index)
AHI 是 InnoDB 根据查询模式自动构建的哈希索引,用于加速等值查询
InnoDB 监控索引的查询频率,当某些索引页被频繁访问时,自动为这些页上的索引键建立哈希索引,将 B+Tree 的随机访问转化为哈希表的 O (1) 访问
Redo 日志是可以理解为数据库的操作备份日志
当执行增删改操作时,数据库会先把修改了什么记录到 Redo 日志里,再去真正修改数据
如果数据库突然崩溃,重启后可以通过 Redo 日志重做之前的操作,保证数据不丢失
比如转账给朋友,Redo 日志会记给朋友账户加 1000 元,就算中途断电,重启后也能根据这条记录补完操作
非唯一二级索引
先简单理解索引:类似书的目录,能快速找到数据位置。
主键索引:最核心的索引,每个值唯一,能直接定位到数据。比如身份证号
二级索引:除了主键外的其他索引,用来辅助查询。比如手机号、姓名
非唯一:指二级索引的值可以重复,比如姓名索引,可能有多个人叫张三
比如查年龄 = 25 的所有用户,如果给年龄建了非唯一二级索引,数据库就不用全表扫描,直接通过索引找到所有符合条件的记录位置,再去查具体数据。
随机 I/O 指读写磁盘时,数据的位置是零散的、不连续的,需要频繁移动磁盘读写头找位置。就像在书架上找书,先找第 5 排的书,再找第 2 排,再找第 8 排,来回跑,效率低。
数据库里如果频繁查询分散的数据,比如随机查不同 ID 的记录,就会产生大量随机 I/O,速度较慢。
顺序 I/O 指读写磁盘时,数据的位置是连续的,磁盘读写头不用来回移动,顺着读 / 写就行。就像找书时,书都按顺序放在第 3 排,从第 1 本读到第 10 本,不用来回跑,效率高。
比如数据库批量写入数据、或读取连续的日志文件,都是顺序 I/O,速度比随机 I/O 快很多。
查询模式指数据库在实际运行中,常用什么样的方式查数据
比如:
有的系统经常查用户 ID=XXX(等值查询)
有的系统经常查年龄> 20 且性别 = 女(范围查询)
有的查询很频繁(热点查询),有的偶尔查一次
数据库会通过统计这些查询模式,优化自己的性能,比如自动建索引
哈希索引是一种通过哈希算法快速定位数据的索引
原理:把索引值(比如用户 ID)通过哈希算法转换成一个数字(哈希值),直接用这个数字定位数据位置,就像用钥匙直接开锁,一步到位
优点:等值查询速度极快,比普通索引快很多
缺点:不支持范围查询,因为哈希值是无序的
B+Tree 是数据库里最常用的索引结构,长得像一棵倒挂的树,专门优化磁盘读写的索引
结构:最上层是根节点,中间是枝节点,最下层是叶子节点(真正存数据位置的地方)。叶子节点之间用链表连起来,方便范围查询
优点:
层数少(通常 3-4 层),查数据时磁盘 I/O 次数少(比如查根节点→枝节点→叶子节点,3 次 I/O 就能找到)
叶子节点有序且连续,支持范围查询
磁盘结构
InnoDB 的磁盘结构负责数据的持久化存储,确保事务的 ACID 特性,尤其是持久性和一致性
InnoDB 的磁盘结构由以下部分组成:
1. 表空间(Tablespaces)
表空间是 InnoDB 存储数据的基本单位,MySQL 8.0 中提供了多种表空间类型以满足不同需求:
表空间类型 | 特点 | 默认文件名 | 主要存储内容 |
---|---|---|---|
系统表空间 | 存储核心元数据 | ibdata1 | 数据字典、undo 日志(老版本)、双写缓冲等 |
独立表空间 | 每张表单独存储 | 表名.ibd | 对应表的数据和索引 |
Undo 表空间 | 专门存储 Undo 日志 | undo_001, undo_002 | 事务回滚和 MVCC 所需的 Undo 日志 |
临时表空间 | 存储临时数据 | ibtmp1 等 | 临时表、排序和哈希操作的中间结果 |
老版本(MySQL 5.7 及之前)Undo 日志默认存储在系统表空间(ibdata1) 中
2. 重做日志(Redo Log)
Redo Log 保障事务持久性,记录了数据页的物理修改,用于数据库崩溃后的恢复。
特点:
物理日志:记录的是数据页的物理变化,如页 X 的偏移量 Y 处修改为 Z,与具体业务逻辑无关
循环写入:Redo Log 文件以固定大小循环写入,当 Redo Log 文件写满后从开头重新开始。
WAL 机制:采用 Write-Ahead Logging 机制,事务提交时先写 Redo Log,再修改数据页,确保即使数据页未刷盘,也能通过 Redo Log 恢复。
Redo Log 的记录逻辑:Redo Log 记录的是数据页的物理修改(比如 页 X 偏移量 Y 改为 Z),但它依赖于数据页本身是完整的。如果数据页已经损坏(半页写),Redo Log 不知道原来的完整页面是什么样,无法基于损坏的页面恢复正确的修改。
3. 撤销日志(Undo Log)
Undo Log 支持事务回滚和 MVCC(多版本并发控制),记录了事务的逻辑反向操作
作用:
事务回滚:当事务执行 ROLLBACK 时,InnoDB 通过 Undo Log 撤销已执行的操作,恢复数据到事务开始前的状态。
MVCC 支持:为读取操作提供一致性视图,当读取数据时,若事务修改,可通过 Undo Log 访问历史版本。
生命周期:Undo Log 在事务提交后不会立即删除,会保留一段时间供 MVCC 读取,之后由后台线程清理。
4. 双写缓冲(Doublewrite Buffer)
双写缓冲用于防止半页写问题,提高了数据写入的可靠性
半页写问题:当数据库写入数据页时,若发生断电等意外,可能导致数据页只写入部分,造成数据页损坏,且无法通过 Redo Log 恢复(Redo Log 基于完整页修改)
解决机制:
写入脏页时,先将完整的页数据写入双写缓冲的磁盘区域(位于系统表空间)
成功写入双写缓冲后,再将页数据写入实际的数据文件位置。
若写入过程中发生故障,恢复时可从双写缓冲读取完整页数据进行修复。
MVCC 是数据库实现高并发读写的核心技术,简单理解就是同一份数据保留多个历史版本,让读写操作不冲突。
举个例子:当修改一条数据时,比如把价格从 100 改成 200,数据库不会直接覆盖旧数据,而是生成一个新的版本存起来,旧版本暂时保留。此时其他用户读取这条数据时,仍然能看到修改前的旧版本(100),新版本(200)提交后新启动的事务可见。
核心作用:解决读写冲突,写操作不阻塞读操作,读操作也不阻塞写操作,同时保证事务隔离性(比如可重复读隔离级别)
依赖的技术:主要通过 Undo 日志存储旧版本数据,通过隐藏列,如 DB_TRX_ID 事务 ID、DB_ROLL_PTR 回滚指针关联不同版本。
一致性视图是 MVCC 中读操作时看到的数据版本快照,可以理解为事务启动时拍下的一张数据快照
核心逻辑:事务启动时,数据库会生成一个视图,记录当前活跃的所有事务 ID(还没提交的事务)。之后这个事务读取数据时,只会看到在视图生成前已提交的事务修改,或者自己修改的数据,看不到视图生成后其他事务的修改
举个例子:
事务 A 启动,生成一致性视图(此时活跃事务只有 A 自己)
事务 B 启动并修改了数据(未提交),事务 A 读取时看不到 B 的修改
事务 B 提交后,事务 A 再次读取,仍然看不到 B 的修改(因为视图生成时 B 还没提交,属于活跃事务)
作用:保证事务在可重复读隔离级别下,多次读取的数据一致,不受其他事务干扰
读已提交(RC):利用 MVCC 实现时,每次执行读操作(如 SELECT)都会生成一个新的一致性视图。因此,事务能看到当前时间点已提交的所有修改,符合读已提交的要求(只能看到已提交的变更)
可重复读(RR):利用 MVCC 实现时,事务启动时生成一个固定的一致性视图,后续所有读操作都基于这个视图。因此,事务内多次读取结果一致,符合可重复读的要求(不受其他事务中途提交的影响)
数据存储与索引机制
InnoDB 的行格式
行格式(Row Format)定义了数据行在磁盘上的存储结构,InnoDB 提供了多种行格式适应不同场景
1. 紧凑行格式(Compact)
MySQL 5.6 之前的默认行格式,存储空间利用率高。
列偏移量以紧凑方式存储,减少冗余。
每行开头存储变长字段长度列表(针对 VARCHAR、TEXT 等变长类型),按列顺序逆序排列。
NULL 值列表,用二进制位图标记哪些列值为 NULL,进一步节省空间。
数据部分按列顺序存储,无冗余偏移量信息。
适用于中小规模数据,对存储空间敏感的表。
Compact 行格式的紧凑在哪里?
NULL 用位图标记:一个二进制位就能标记一列是否为 NULL,比存NULL字符串省空间。
数据直接按顺序存:没有冗余的偏移量,比如这列数据从第 X 字节开始,直接紧凑排列。
Compact 行格式的长度列表采用逆序存放,是为了方便从总行长推算各字段起始位置,从而避免存储冗余的偏移量表,间接实现了存储更紧凑。
2. 冗余行格式(Redundant)
MySQL 5.0 之前的默认行格式
每行开头存储所有列的偏移量,空间利用率低,但便于解析。
现在主要用于兼容性,不推荐使用。
3. 动态行格式(Dynamic)
MySQL 5.7 及以上版本的默认行格式,是 Compact 格式的增强版。
针对大字段(VARCHAR > 255、TEXT、BLOB)采用溢出存储策略
当行数据总大小超过页大小(16KB)的一半时,大字段数据会被迁移到溢出页(Overflow Page)。原数据行中仅保留 20 字节指针(指向溢出页地址和长度),大幅减少主数据页的空间占用。解决了大字段导致的页碎片化问题,提升缓存效率。
规则:
字段 ≤ 40 字节 → 永远存原行,不溢出
字段 > 40 字节 → 原行先留 20 字节指针,是否实际溢出看行总大小是否 > 8KB如果一行里有多个字段都大于 40B,InnoDB 会优先把最长的字段溢出,直到行总大小 ≤8KB 为止
4. 压缩行格式(Compressed)
在 Dynamic 格式基础上增加页级压缩机制。
数据页写入磁盘前会通过 zlib 算法压缩,默认压缩比约 50%。读取时先解压到内存缓冲池,后续访问直接使用内存中的未压缩版本。
适用于存储大量数据、磁盘 IO 成本高的场景。优点是节省空间,缺点是压缩/解压增加 CPU 消耗。
只有Redundant行格式是非紧凑的,过时的,其他的都是较新的行格式,紧凑的
B+Tree 索引结构
InnoDB 使用 B+Tree 作为索引结构,支持高效的范围扫描和顺序访问。
聚簇索引(Clustered Index)
每张 InnoDB 表必须有且只有一个聚簇索引。
默认情况下,主键就是聚簇索引:
索引的非叶子节点:存储主键值
索引的叶子节点:存储整行数据
如果表没有主键:
会选择第一个非空唯一索引作为聚簇索引
如果没有唯一索引,则会生成一个隐藏的 row_id
特点:表数据按主键顺序存储,范围查询效率高
聚簇索引:数据和索引绑在一起的索引
可以把聚簇索引理解成带数据的目录,就像一本书的目录不仅有章节标题,还直接把章节内容印在目录页上。
例子:
假设有一张学生表,主键是学号。聚簇索引就像按学号排序的目录,目录的每个叶子节点不仅有学号,还直接包含该学生的姓名、年龄、成绩等所有信息。用学号查数据时,找到索引位置就能直接拿到完整数据,速度很快。
二级索引(Secondary Index / Non-Clustered Index)
除聚簇索引外的索引都是二级索引。
叶子节点存储:
索引列值
对应行的主键值(而不是物理地址)
查找过程:先找到主键,再回表到聚簇索引定位整行数据。
如果查询所需的所有字段都包含在二级索引中,这个二级索引就成为了覆盖索引,查询时可以直接使用该索引获取数据,避免回表,从而提高性能
二级索引(也叫辅助索引)是只指路的目录,就像一本书除了主目录,还有一个按关键词排序的辅助目录,目录里只有这个关键词在主目录的第 X 页,需要再通过主目录找内容。
特点:
一张表可以有多个二级索引,比如按姓名、年龄分别建索引
索引不存完整数据,只存指针:叶子节点存储的是主键值(聚簇索引的键),而不是整行数据。
查询需要回表:通过二级索引找到主键后,必须再去聚簇索引中查完整数据,除非是覆盖索引,即索引包含了查询所需的所有字段
例子:
还是学生表,按姓名建了二级索引。这个索引的叶子节点存储的是姓名 + 学号,比如 “张三 → 1001”。用姓名查 “张三的成绩” 时,步骤是:先在二级索引中找到 “张三” 对应的学号 1001
再到聚簇索引中用学号 1001 找到完整数据,获取成绩
索引性能优化
优化索引时应注意:
1. 联合索引设计:最左前缀原则
联合索引(多列索引)的生效顺序严格遵循最左前缀匹配,即索引仅对查询条件中从最左列开始的连续匹配生效。
反例:对 (a, b, c) 建立索引,WHERE b = 1 AND c = 2 无法使用索引。
正例:WHERE a = 1、WHERE a = 1 AND b = 2、WHERE a = 1 AND b = 2 AND c = 3 均可使用索引。
设计建议:将区分度高的列(如身份证号)放在左侧,频繁过滤的列优先。
2. 避免过度索引:过多索引会拖慢写操作,因为每次插入/更新都需要维护多个索引。
3. 覆盖索引:通过包含所需字段,减少回表次数。
4. 索引维护:碎片多时可以 OPTIMIZE TABLE 或重建索引。
事务处理与 ACID
InnoDB 提供了完整的事务支持,满足 ACID 四大特性:
原子性(Atomicity):事务要么全部执行,要么全部回滚。
一致性(Consistency):事务完成后,数据库从一个一致状态转到另一个一致状态。
隔离性(Isolation):并发事务之间逻辑上相互独立。
持久性(Durability):事务提交后的更改持久保存,即使系统崩溃也能恢复。
多版本并发控制(MVCC)
问题背景:
在并发场景下,如果没有 MVCC,所有读取都要加锁,读和写会严重互相阻塞
解决思路:
MVCC 通过保存行的多个版本,让读操作读取历史版本,而写操作只操作最新版本,从而避免大多数读写冲突。所以叫 Multi-Version Concurrency Control(多版本并发控制)
MVCC 的底层实现
1. 行的隐藏列
在 InnoDB 每一行记录后面,都有几个 隐藏列:
trx_id:最后一次修改该行的事务 ID
roll_ptr:指向 undo log(撤销日志)的指针,可以沿着它找到该行的历史版本
可能还有一个 row_id,当表没有主键时自动生成,和 MVCC 无关
2. Undo Log
每次修改(UPDATE / DELETE)都会把旧值写入 Undo Log
Undo Log 存的是逻辑反操作(旧版本),并且形成一个链表(版本链)
如果需要历史版本,就顺着 roll_ptr 找旧值
3. Read View(读视图)
当事务开始时,会生成一个 Read View,里面记录了:
当前活跃事务 ID 的集合
本事务的最小活跃 ID、最大分配 ID
事务在查询时,会根据 Read View 判断某个版本是否对我可见
如果版本太新(由未提交事务产生),对我不可见
如果版本较旧,或者由已提交事务产生,对我可见
快照读 vs 当前读
快照读(Snapshot Read)
普通 SELECT 就是快照读
读到的是 Read View 生成时可见的版本(旧数据),不会加锁
当前读(Current Read)
SELECT … FOR UPDATE、UPDATE、DELETE 等,必须读到最新版本
会加锁(行锁/间隙锁/next-key 锁),防止并发修改
读已提交(RC):每次执行读操作(如 SELECT)都会生成一个新的一致性视图。事务只能读到其他事务已经提交的数据。
可重复读(RR):事务启动时生成一个固定的一致性视图,后续所有读操作都基于这个视图。事务里多次读取同一行结果要一致,还要避免幻读。
快照读(Snapshot Read)
普通 SELECT,不加锁,通过 Read View + MVCC 决定能看到哪些版本
在 RR 下:整个事务共用一个 Read View ,避免不可重复读、幻读
在 RC 下:每次查询新建一个 Read View ,可能出现不可重复读、幻读
当前读(Current Read)
需要最新数据,必须加锁:SELECT … FOR UPDATE、UPDATE、DELETE、INSERT
在 RR 下:使用 Next-Key Lock 锁住范围,避免幻读
在 RC 下:只加记录锁,不加间隙锁 ,可能出现幻读
锁机制
表级锁(Table Lock)
锁整个表,粒度大,并发性低。
典型场景:ALTER TABLE、LOCK TABLES。
InnoDB 一般不会主动使用表锁,除非特殊语句。
行级锁(Row Lock)
通过索引项实现,不是直接锁物理行
并发性高,是 InnoDB 默认的核心锁。
包含以下几种:
1. 记录锁(Record Lock)
锁定索引上的一个具体记录
例子:SELECT * FROM t WHERE id=5 FOR UPDATE; → 只锁住 id=5
2. 间隙锁(Gap Lock)
锁定索引值之间的“空隙”,防止其他事务在这个范围插入新记录
例子:SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE;
会锁定 10 和 20 之间的空隙,防止别人插入 id=15
3. Next-Key Lock
= 记录锁 + 间隙锁
作用:既锁定已有记录,又锁定前后的间隙
是 InnoDB 在 RR 隔离级别下的默认锁方式,用来避免幻读
4. 插入意向锁(Insert Intention Lock)
一种特殊的间隙锁
当事务打算插入一条新记录时,先在目标间隙上加插入意向锁
允许多个事务同时持有(不冲突),只有真正插入时才判断是否和 Next-Key 锁冲突
锁与隔离级别的关系
RC(Read Committed)
快照读:不加锁,每次读新视图
当前读:主要用记录锁,一般不加间隙锁
可能出现幻读(新插入的数据被读到)
RR(Repeatable Read,默认)
快照读:整个事务用同一个 Read View
当前读:使用 Next-Key Lock(记录锁 + 间隙锁)
可以避免幻读
幻读指的是:
一个事务按照相同条件两次读取,第二次读到了第一次没有的行和不可重复读的区别:
不可重复读:同一行被别的事务修改,读出来的值前后不一致
幻读:同一范围内出现了新增的行