InnoDB 工作原理

上一篇我们介绍了数据库的发展历史以及 NewSQL 的设计思想。为了在做数据库选型和表结构设计时有更清晰的判断,本篇先回到最常见的 MySQL InnoDB,看看它是如何通过索引、缓存、日志、锁和 MVCC 支撑事务语义的。

理解 InnoDB 并不是为了背实现细节,而是为了在建模时知道哪些设计会放大成本。比如主键是否有序,会影响聚簇索引的写入;索引是否覆盖查询,会影响回表次数;事务范围是否过大,会影响锁等待、undo 清理和主从延迟。

InnoDB 解决什么问题

MySQL 是 Server 层加存储引擎的架构。SQL 解析、优化器、权限、连接管理、binlog 等能力属于 MySQL Server 层;数据如何存储、如何加锁、如何恢复事务,主要由存储引擎负责。InnoDB 是 MySQL 最常用的事务型存储引擎,它主要提供以下能力:

  • 事务支持:提供 ACID 语义,支持提交、回滚和崩溃恢复。
  • 行级锁:相比表级锁,能在 OLTP 场景下提供更高的并发能力。
  • MVCC:通过多版本并发控制,让读写冲突尽量减少。
  • 聚簇索引:数据行按照主键组织,主键就是数据的物理访问入口。
  • 崩溃恢复:通过 redo log、undo log、checkpoint 等机制保证数据可恢复。

这些能力合在一起,支撑了我们平时习以为常的能力:执行一条 UPDATE,数据库既要让并发事务看到正确的数据版本,又要保证机器宕机后数据不会变成半提交状态。

存储结构

InnoDB 的数据不是简单地一行一行平铺在文件里,而是按照页来组织。默认情况下,一个页大小为 16KB,页是磁盘和内存之间交换数据的基本单位。

从逻辑上看,InnoDB 的存储层级可以理解为:

  1. 表空间:存放表和索引数据的逻辑空间。
  2. :表、索引、回滚段等不同用途的数据区域。
  3. :由连续的数据页组成,默认一个区为 1MB。
  4. :InnoDB 读写磁盘的基本单位,默认 16KB。
  5. :真正的业务数据记录。

这套结构意味着数据库不是“只读一行”,而是把包含这一行的数据页加载到 Buffer Pool 中。一次随机查询如果无法命中缓存,就需要先读取整页;一次范围查询如果数据在 B+Tree 上相邻,就可以顺着页链表连续读取,成本会低很多。

聚簇索引与二级索引

InnoDB 使用 B+Tree 组织索引。B+Tree 的特点是所有数据都在叶子节点,非叶子节点只保存搜索路径,因此非常适合磁盘页结构:树高较低,单次查询通常只需要少量随机 IO。

InnoDB 的特殊之处在于它采用聚簇索引。表数据本身就是按照主键构建的一棵 B+Tree,叶子节点存放完整的数据行。所以:

  • 通过主键查询,可以直接在聚簇索引中找到完整记录。
  • 二级索引的叶子节点不存完整数据行,而是存二级索引键和主键值。
  • 通过二级索引查询非索引列时,需要先找到主键,再回到聚簇索引查询完整数据,这个过程就是回表。

因此主键设计会直接影响 InnoDB 的数据组织方式。业务上常见的几个原则是:

  • 主键尽量短,二级索引都会携带主键,主键过大会放大所有二级索引空间。
  • 主键尽量稳定,主键变更会导致数据在聚簇索引中移动。
  • 主键尽量有序,完全随机的主键会增加页分裂和写入放大。
  • 不要把业务含义过强的字段作为物理主键,业务规则变化会牵连底层存储。

这也是很多系统会使用 BIGINT 类型的代理主键,再通过唯一索引约束业务唯一性的原因。

Buffer Pool

Buffer Pool 是 InnoDB 最重要的内存结构。数据页、索引页、undo 页等都会被缓存到 Buffer Pool 中,尽量把磁盘随机 IO 转换为内存访问。

当查询读取某个页时,InnoDB 会先查 Buffer Pool:

  1. 如果页已经在内存中,直接读取。
  2. 如果页不在内存中,从磁盘加载到 Buffer Pool。
  3. 如果 Buffer Pool 空间不足,根据淘汰策略选择部分页刷出或淘汰。

当事务修改数据时,InnoDB 通常不会立即把数据页写回磁盘,而是先修改内存中的页,把它标记为脏页,再由后台线程异步刷盘。这样可以把多次随机写合并成更可控的批量写,但也带来了一个问题:如果脏页还没刷盘机器就宕机了,如何恢复?

答案就是日志。

Redo Log 与 Undo Log

InnoDB 的事务恢复主要依赖两类日志:redo log 和 undo log。

redo log 记录的是“对数据页做了什么修改”,用于保证事务的持久性。事务提交时,只要 redo log 已经安全落盘,即使对应的数据页还没刷盘,数据库崩溃后也可以通过 redo log 重放修改。

undo log 记录的是“修改前的数据版本”,主要用于两个场景:

  • 事务回滚时,根据 undo log 撤销已经执行的修改。
  • MVCC 快照读时,根据 undo log 还原出旧版本数据。

可以把 redo log 理解为“向前恢复”,把 undo log 理解为“向后回滚”。它们共同支撑了事务的原子性、持久性和一致读。

Binlog 与两阶段提交

除了 InnoDB 自己的 redo log,MySQL Server 层还有 binlog。binlog 记录逻辑变更,主要用于主从复制、数据恢复和审计。

这就带来一个一致性问题:一次事务提交既要写 InnoDB redo log,又要写 MySQL binlog。如果其中一个成功另一个失败,主库和从库、存储引擎和 Server 层看到的状态就可能不一致。

MySQL 通过两阶段提交协调 redo log 和 binlog:

  1. InnoDB 写 redo log prepare。
  2. Server 层写 binlog。
  3. InnoDB 写 redo log commit。

崩溃恢复时,MySQL 可以根据 redo log 和 binlog 的状态判断事务应该提交还是回滚。对于业务开发来说,这解释了为什么事务提交不是一次简单写盘,而是跨存储引擎日志和复制日志的协同过程。

MVCC 与一致读

MVCC 是 InnoDB 支持高并发读写的关键。它的目标是让读操作尽量不阻塞写操作,写操作也尽量不阻塞普通快照读。

InnoDB 每行记录中会维护一些隐藏字段,比如创建或修改该行的事务 ID,以及指向 undo log 的回滚指针。当事务执行一致性读时,InnoDB 会生成一个 Read View,用来判断哪些事务版本对当前事务可见。

如果当前记录版本不可见,InnoDB 就沿着 undo log 构造历史版本,直到找到一个对当前事务可见的版本。这样,一个事务即使正在修改某行数据,其他事务也可以读取到它们应该看到的旧版本。

不同隔离级别下,Read View 的生成时机不同:

  • 读已提交:每次一致性读都会生成新的 Read View,因此同一事务内两次查询可能看到不同结果。
  • 可重复读:事务第一次一致性读时生成 Read View,后续复用,因此同一事务内普通查询结果相对稳定。

MySQL InnoDB 默认使用可重复读隔离级别,并通过 next-key lock 等机制缓解幻读问题。但这并不意味着所有查询都不会遇到并发异常,尤其是范围更新、唯一约束冲突、当前读等场景仍然需要谨慎设计。

锁模型

InnoDB 的锁不是只有“行锁”一种。常见锁包括:

  • 记录锁:锁定某条索引记录。
  • 间隙锁:锁定索引记录之间的间隙,防止其他事务插入新记录。
  • next-key lock:记录锁加间隙锁,用于范围查询和范围更新。
  • 插入意向锁:多个事务插入同一间隙内不同位置时,用来减少无谓冲突。
  • 意向锁:表级锁,用来表示事务即将对表中某些行加共享锁或排他锁。

InnoDB 的行锁是加在索引上的。如果查询条件没有命中合适索引,数据库可能扫描大量记录并加锁,甚至表现得像锁住了整张表。因此很多线上“锁等待”问题,根源并不是事务逻辑复杂,而是缺少正确的索引。

写入路径

一次典型的更新大致可以简化为:

  1. 根据索引找到目标记录所在的数据页。
  2. 如果数据页不在 Buffer Pool 中,先从磁盘加载。
  3. 生成 undo log,用于回滚和 MVCC。
  4. 修改 Buffer Pool 中的数据页,并将其标记为脏页。
  5. 写入 redo log,保证崩溃后可以恢复。
  6. 提交事务时协调 redo log 和 binlog。
  7. 后台线程在合适时机将脏页刷回磁盘。

从这个路径可以看到,写入性能不只取决于“写一行数据”本身,还取决于索引数量、页分裂、日志刷盘、事务大小、主从复制和脏页刷新等因素。

对建模的启发

理解 InnoDB 后,很多建模规范就不再是教条:

  • 主键短而稳定,是为了降低聚簇索引和二级索引的空间成本。
  • 尽量让主键有序,是为了减少随机写和页分裂。
  • 高频查询需要合适的联合索引,是为了减少扫描和锁范围。
  • 避免大事务,是为了减少 undo log、锁等待和复制延迟。
  • 避免过多索引,是因为每个索引都会增加写入和维护成本。
  • 谨慎使用范围更新,是因为它可能触发 next-key lock,扩大锁冲突。
  • 字段类型越精确越好,是因为类型大小会影响行大小、索引大小和缓存命中率。

数据库建模不是把对象属性机械地映射成表字段,而是要把业务访问模式翻译成存储引擎能够高效执行的结构。InnoDB 给了我们可靠的事务能力,但可靠性背后始终有成本,建模的职责就是让这份成本落在真正需要的地方。

总字数:2738