InnoDB 引擎与索引事务:B+ 树、聚簇索引与隔离级别
基于 MySQL 8.4 LTS / 9.x · 核于 2026-08
速查
- 存储引擎分层:server 层(连接/解析/优化/执行,跨引擎)+ InnoDB 引擎层(行锁/MVCC/崩溃恢复/外键,真正存数据)。
ENGINE=InnoDB选引擎,95% 场景用它。 - InnoDB 四大支柱:①行锁 + MVCC(读写不冲突);②崩溃恢复(redo log 持久化、undo log 回滚,WAL 先写日志);③外键约束(跨表引用完整);④聚簇索引(主键索引与数据一体)。
- B+ 树索引:数据只在叶子节点(非叶子节点只路由),叶子用双向链表串成有序。扇出大→3-4 层存亿级行;链表→范围查询顺着扫。
- 聚簇索引 vs 二级索引:聚簇索引(主键)叶子存整行;二级索引叶子存主键值,查二级索引后要回表(去聚簇索引取整行)。覆盖索引(索引含查询全部列)避免回表,
EXPLAIN见Using index。 - 联合索引最左前缀:
INDEX(a,b,c)支持a/a,b/a,b,c,不支持跳过a的b/c。顺序按「区分度高、查询频繁、常作范围」排。 - 索引下推 ICP(5.6+):把
WHERE对二级索引列的过滤下推到引擎层,减少回表次数。 - ACID:原子性(undo log 回滚)、持久性(redo log 刷盘)、隔离性(锁+MVCC)、一致性(AID 共同保证)。
- 四种隔离级别:READ UNCOMMITTED < READ COMMITTED(PG 默认)< REPEATABLE READ(MySQL 默认)< SERIALIZABLE。
- MySQL 的 RR 用间隙锁:RR 下用**临键锁(next-key lock = 行锁 + 间隙锁)**避免幻读,比 Oracle/PG 在 RR 下仍有幻读更严格。
- MVCC:每行有 undo log 版本链,事务开始生成一致性视图,读不加锁看快照——读写不冲突。代价:长事务→undo 膨胀→purge 跟不上→空间不回收。
- 锁体系:共享/排他锁(S/X);行锁分记录锁(锁单行)、间隙锁(锁区间,防插入)、临键锁(记录+前区间);意向锁(IS/IX,表级,标「有行锁」加快冲突判断)。
- 死锁:MySQL 自动检测并回滚代价小的事务。预防:固定加锁顺序、小事务、
FOR UPDATE谨慎。
一、InnoDB 存储结构:表空间、段、区、页
InnoDB 把数据组织成层次结构,理解它能解释很多调优现象:
表空间(ibd 文件)
└ 段(segment):索引段 + 数据段 + 回滚段
└ 区(extent):1MB = 64 个页
└ 页(page):16KB(默认),磁盘 IO 的最小单位
└ 行(row):每行有事务 ID + 回滚指针 + 数据- **页(page)**是 InnoDB 磁盘 IO 的最小单位——一次读写一页(16KB),不是一行。所以
innodb_buffer_pool也以页为单位缓存。 - 行内有隐藏列:每行除用户数据外,还有
DB_TRX_ID(最后修改它的事务 ID)、DB_ROLL_PTR(指向 undo log 的回滚指针)——这是 MVCC 的基础。 - innodb_buffer_pool_size:缓存热页的内存区,最重要的参数,设可用内存的 50-70%。命中率应 > 99%。
二、B+ 树索引:为什么不是 B 树或红黑树
InnoDB 索引统一用 B+ 树(B-plus tree),关键区别:
| 结构 | 数据存哪 | 范围查询 | 树高 | 适用 |
|---|---|---|---|---|
| B 树 | 所有节点都存数据 | 需中序遍历,慢 | 中 | 文件系统 |
| B+ 树 | 只在叶子,叶子双向链表 | 顺着链表扫,极快 | 矮(扇出大) | 数据库索引 |
| 红黑树 | 每节点一值 | 不适合范围 | 高 | 内存结构(C++ map) |
- 为什么 B+ 树:①扇出大——非叶子节点不存数据只存键,一个 16KB 页能存几百个键,3-4 层就能索引亿级行(每次查 3-4 次磁盘 IO);②范围查询快——叶子节点链表,
WHERE id BETWEEN 100 AND 200找到 100 后顺着链表扫到 200 即可。 - 哈希索引不是默认:InnoDB 有「自适应哈希索引」(AHI),对热点查询自动建内存哈希,但不可手动控制。需要精确点查的内存场景用 Memory 引擎或 Redis。
三、聚簇索引与二级索引:回表与覆盖索引
InnoDB 的索引按「叶子存什么」分两类:
聚簇索引(主键索引):
[非叶子: 主键路由]
│
[叶子: 主键值 + 整行数据] ← 数据就存在主键索引的叶子里
二级索引(如 INDEX(name)):
[非叶子: name 路由]
│
[叶子: name 值 + 主键值] ← 只存主键,不存整行- 聚簇索引只有主键:一张 InnoDB 表只有一个聚簇索引(主键)。没有显式主键时,InnoDB 选第一个 NOT NULL 唯一索引;都没有就生成隐藏 6 字节
ROW_ID(不推荐,不可控)。 - 回表(lookup):用二级索引查到主键值后,还要去聚簇索引取整行——多一次 B+ 树查找。如果查询只需索引列与主键,则无需回表,称为覆盖索引。
- 覆盖索引:让索引覆盖查询所需全部列,
EXPLAIN的Extra显示Using index。例如SELECT id, name FROM user WHERE name=?,若name是二级索引,id(主键)已在叶子,覆盖索引成立,避免回表。 - 主键设计建议:用自增整型(
BIGINT AUTO_INCREMENT)——顺序插入,叶子节点追加,页分裂少;避免 UUID 主键(随机插入导致频繁页分裂、索引膨胀)。
四、联合索引与最左前缀
INDEX(a, b, c) 是一颗 B+ 树,按 a → b → c 排序。最左前缀原则:只能从最左列开始连续匹配:
sql
-- 能用上 INDEX(a, b, c)
WHERE a = 1 -- 用 a
WHERE a = 1 AND b = 2 -- 用 a, b
WHERE a = 1 AND b = 2 AND c = 3 -- 用 a, b, c
WHERE a = 1 AND c = 3 -- 只用 a(c 跳过了 b,无法用)
-- 用不上 INDEX(a, b, c)
WHERE b = 2 -- 跳过了 a
WHERE b = 2 AND c = 3 -- 跳过了 a- 列顺序原则:①区分度高的放前面(过滤掉更多行);②查询最频繁的放前面;③范围查询列放最后(范围后的列用不上索引)。
- 索引下推(ICP,5.6+):
WHERE name LIKE '张%' AND age > 18,若INDEX(name, age),引擎层先用 name 过滤再下推 age 过滤,减少回表。
五、ACID 与四种隔离级别
事务的 ACID 是一致性的保证:
| 性质 | 含义 | 实现 |
|---|---|---|
| 原子性(Atomicity) | 全做或全不做 | undo log(回滚未提交事务) |
| 一致性(Consistency) | 合法状态→合法状态 | AID 共同保证 + 约束(外键/唯一/检查) |
| 隔离性(Isolation) | 并发事务互不干扰 | 锁(写)+ MVCC(读) |
| 持久性(Durability) | 提交后断电不丢 | redo log(WAL,先写日志再改数据页) |
并发事务会带来三类问题:脏读(读到别的事务未提交的数据)、不可重复读(同一事务两次读同一行结果不同)、幻读(同一事务两次范围查询结果集不同)。
四种隔离级别对应能避免哪些问题:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 避免 | 可能 | 可能 |
| REPEATABLE READ(MySQL 默认) | 避免 | 避免 | 避免(间隙锁) |
| SERIALIZABLE | 避免 | 避免 | 避免 |
- MySQL 默认 REPEATABLE READ——比 Oracle/PostgreSQL 默认的 READ COMMITTED 更严格。MySQL 在 RR 下用间隙锁/临键锁避免幻读,其他数据库在 RR 下仍可能有幻读。
- 生产建议:多数业务用默认 RR 即可;对延迟敏感且能接受不可重复读的,可降到 RC(减少间隙锁、提升并发)。
六、MVCC:多版本并发控制
MVCC 让「读不阻塞写、写不阻塞读」,是高并发基础:
- 版本链:每行的
DB_ROLL_PTR指向 undo log 里的旧版本,形成「当前版本 → 旧版本 → 更旧版本」链表。 - 一致性视图(read view):事务执行
SELECT时生成一个视图,记录「当时活跃(未提交)的事务 ID 列表」。读操作根据视图判断「该看哪个版本」——只看视图生成时已提交的版本。 - RC vs RR 的 MVCC 差异:RC 每次
SELECT都生成新视图(所以能看到别的事务新提交的,导致不可重复读);RR 事务第一次SELECT时生成视图并复用整个事务(所以可重复读)。 - 代价——长事务是杀手:事务不结束,它用到的 undo 版本就不能被 purge(清理)线程回收 → undo 表空间膨胀 → 磁盘占用涨、历史版本链变长、查询变慢。生产务必避免长事务(
information_schema.innodb_trx查长事务)。
七、锁:行锁、间隙锁与临键锁
InnoDB 的锁体系(RR 下):
| 锁类型 | 锁什么 | 用途 |
|---|---|---|
| 记录锁(Record Lock) | 锁单行 | 命中行的写 |
| 间隙锁(Gap Lock) | 锁两行之间的区间(不含端点) | 防止区间内插入新行(防幻读) |
| 临键锁(Next-Key Lock) | 记录 + 它前面的间隙 | RR 默认,记录锁 + 间隙锁的组合 |
| 意向锁(IS/IX) | 表级 | 标记「表内有行锁」,加快表锁冲突判断 |
- 共享锁(S)/排他锁(X):读加 S(
LOCK IN SHARE MODE),写加 X(FOR UPDATE)。S 兼容 S,X 与任何锁互斥。 - 死锁:事务 A 锁了行 1 等行 2,事务 B 锁了行 2 等行 1。InnoDB 自动检测死锁并回滚代价较小的事务(undo 量少的)。预防:①固定加锁顺序;②事务尽量短;③
FOR UPDATE谨慎用。
下一步
InnoDB 与索引事务讲透后,下一个核心是复制、JSON 与性能调优——binlog 三种格式与 GTID、半同步/组复制、JSON 类型与函数、连接池、EXPLAIN 实战与慢查询分析。