目录
关系模型 InnoDB 存储引擎架构 B+ 树索引 事务 ACID 四种隔离级别 锁机制 MVCC 实现原理 redo / undo / binlog
01

关系模型

背景 为什么需要关系模型?

早期数据库使用 层次模型(IMS)或 网状模型(IDMS),数据通过指针连接,查询需要编写复杂的导航式代码。关系模型由 Edgar Codd 在 1970 年提出,核心思想是:数据以二维表形式呈现,表与表之间通过共同字段关联,而不需要使用物理指针。

第一性原理: 关系模型的本质是「逻辑数据独立性」——数据存储方式与数据访问方式解耦。用户只需用 关系代数 描述「想要什么」,而不是「如何找到它」。这种声明式访问范式,使得数据库系统可以独立优化数据存储和访问路径,而无需修改应用代码。

原理 关系 · 元组 · 键 · 范式

关系 (Relation) 即二维表,每一行是一个 元组 (Tuple),每一列是一个 属性 (Attribute)。关系模型建立在集合论基础上:关系是 笛卡尔积的子集

关系模型: 二维表结构 学号 (PK) 姓名 课程号 (FK) 成绩 学分 2024001 张三 CS101 92 3 2024001 张三 MATH202 85 4 2024002 李四 CS101 78 3 2024003 王五 PHY101 88 4 图:关系表 — 行(元组)与列(属性),PK 为主键,FK 为外键
图:关系表 — 行(元组)与列(属性),PK 为主键,FK 为外键

键 (Key):

  • 主键 (Primary Key): 唯一标识一行,且不能为空
  • 外键 (Foreign Key): 指向另一个表的主键,建立表间关联
  • 候选键 (Candidate Key): 能唯一标识行的最小属性集

范式 (Normal Forms): 关系模型的规范化设计原则,减少数据冗余和异常:

  • 1NF: 属性不可再分(原子性)
  • 2NF: 消除非主属性对主键的部分函数依赖
  • 3NF: 消除非主属性对主键的传递函数依赖
  • BCNF: 消除主属性对候选键的部分依赖
▸ SQL · 关系表定义与查询
-- 创建学生表 (关系) CREATE TABLE students ( student_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, major VARCHAR(50) ); -- 创建课程表 CREATE TABLE courses ( course_id VARCHAR(20) PRIMARY KEY, title VARCHAR(100) NOT NULL, credit INT ); -- 创建选课表 (关联表,外键引用) CREATE TABLE enrollments ( student_id INT, course_id VARCHAR(20), grade INT, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES students(student_id), FOREIGN KEY (course_id) REFERENCES courses(course_id) ); -- 声明式查询:JOIN 关联表 SELECT s.name, c.title, e.grade FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id WHERE s.major = 'CS';

演进 层次/网状 → 关系 → NoSQL → NewSQL

  • 层次/网状模型 (1960s): 数据通过指针连接,查询需导航式编程,复杂且难以维护
  • 关系模型 (1970): 声明式查询 (SQL),逻辑与物理分离,成为数据库主流
  • NoSQL (2000s): 放弃关系模型,支持非结构化数据,牺牲一致性换取可扩展性
  • NewSQL (2010s): 在分布式系统中恢复关系模型,同时提供高可扩展性和强一致性
"关系模型的最大贡献是『数据独立性』——用户只需表达『要什么』,不用关心『怎么存』。这个设计让数据库系统能够独立演进,而不影响上层应用。"
—— 数据库设计哲学

取舍 设计中的权衡

📊 规范化 vs 性能
高度规范化减少冗余,但查询需要更多 JOIN,影响性能。实际设计中常在 3NF 基础上保留适当冗余(反范式化)以提升查询效率。
🔗 声明式 vs 可优化性
SQL 声明式查询让数据库优化器决定执行计划,但优化器可能选择非最优计划。用户可以通过 hints 或重新编写查询干预优化。
⚡ 强模式 vs 灵活性
关系模型要求预定义模式,严格约束数据类型,保障数据完整性。但面对变化频繁的数据结构,模式修改代价高,NoSQL 提供了另一种选择。
02

InnoDB 存储引擎架构

背景 为什么 MySQL 默认使用 InnoDB?

MySQL 早期默认使用 MyISAM 存储引擎,它不支持事务、外键,只支持表级锁,崩溃后需要修复。InnoDB 由 Innobase Oy 公司开发,2005 年被 Oracle 收购,成为 MySQL 5.5 的默认存储引擎。它提供了事务支持、行级锁、外键约束、MVCC 和崩溃恢复等关键特性。

第一性原理: InnoDB 的设计核心是「磁盘与内存之间的高效数据管理」。它使用 页 (Page) 作为磁盘读写单位,通过 缓冲池 (Buffer Pool) 缓存热点数据,使用 聚簇索引 (Clustered Index) 存储主键与数据行,从而在事务 ACID 和并发性能之间取得平衡。

原理 页 · 表空间 · 聚簇索引 · 缓冲池

页 (Page) 是 InnoDB 的 I/O 基本单位,默认 16KB。页内包含数据记录、页头、页尾,通过链表组织起来。

InnoDB 架构概览 内存层 (Memory Layer) 缓冲池 (Buffer Pool) 缓存页 · 索引页 · 数据页 日志缓冲区 (Log Buffer) redo log buffer 后台线程 Master Thread · IO Thread 磁盘层 (Disk Layer) 表空间 (Tablespace) .ibd 文件 · 聚簇索引 数据行 · 索引页 重做日志 (Redo Log) ib_logfile0 · ib_logfile1 崩溃恢复 撤销日志 (Undo Log) MVCC · 回滚 图:InnoDB 内存与磁盘层次结构,缓冲池与日志机制
图:InnoDB 内存与磁盘层次结构,缓冲池与日志机制

聚簇索引 (Clustered Index): InnoDB 将主键与数据行存储在一起,数据行的物理顺序按照主键排列。这意味着:

  • 主键查询只需一次 I/O 就能获取整行数据
  • 非主键索引(二级索引)存储主键值,而非数据行指针
  • 当主键不是顺序插入时,会导致页分裂,影响性能
▸ SQL · InnoDB 表结构查看
-- 查看表的存储引擎 SHOW TABLE STATUS WHERE Name = 'users'; -- 查看 InnoDB 状态 SHOW ENGINE INNODB STATUS\G -- 设置缓冲池大小 (全局变量) SET GLOBAL innodb_buffer_pool_size = 2147483648; -- 2GB -- 查看缓冲池使用情况 SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool_%';

演进 MyISAM → InnoDB → 新存储引擎

  • MyISAM (1990s): 不支持事务、外键,表级锁,崩溃后需要修复,读性能高但写性能差
  • InnoDB (2001): 支持事务、行级锁、MVCC、外键、崩溃恢复,成为 MySQL 默认引擎
  • MyRocks (2016): Facebook 开发的基于 RocksDB 的存储引擎,高压缩率,适合写密集型场景
  • Rapid (2021): 针对内存优化的新引擎,支持极高并发
"InnoDB 的成功在于『提供企业级特性而不牺牲性能』。它将事务、锁、MVCC 集成到存储引擎层,而不是在 SQL 层模拟,从而实现了高性能的事务支持。"
—— 存储引擎设计哲学

取舍 设计中的权衡

💾 内存 vs 磁盘
缓冲池越大,缓存命中率越高,但占用更多内存。InnoDB 允许动态调整缓冲池大小,在内存和 I/O 之间平衡。
🔑 聚簇索引 vs 非聚簇
聚簇索引让主键查询更快,但二级索引查询需要两次 I/O(二级索引 → 主键 → 数据)。非聚簇索引直接指向数据行,但数据行可能不按索引顺序存储。
📄 页大小 vs 存储效率
默认 16KB 页适合一般场景,对于小记录(如只存 ID)存在空间浪费。较小页(4KB)更适合 OLTP 场景,但索引层级更多。
03

B+ 树索引

背景 为什么是 B+ 树而不是二叉树?

数据库索引需要解决的核心问题是:如何在磁盘上高效地查找、插入、删除数据。磁盘 I/O 比内存慢几个数量级,所以索引结构的目标是最小化 I/O 次数。二叉查找树(BST)每层一次 I/O,查找 100 万条记录需要约 20 次 I/O(2^20 ≈ 100 万),而 B+ 树只需 3-4 次。

第一性原理: B+ 树的核心设计是「利用磁盘块的大小优化 I/O」。每个节点(页)的大小与磁盘块对齐,一个节点存储大量关键字(高扇出),从而大幅降低树的高度。同时,所有数据都存储在叶子节点,内部节点只存储索引键,最大化扇出。叶子节点通过链表连接,支持范围查询的高效扫描。

原理 高扇出 · 叶子链表 · 分裂 · 合并

B+ 树的关键特征:

  • 多路搜索树: 每个内部节点有 m 个孩子(m 通常几百到上千),m 称为扇出 (Fanout)
  • 数据只在叶子节点: 内部节点只存储索引键,用于导航
  • 叶子节点链表: 叶子节点按顺序通过指针连接,支持范围查询和顺序遍历
  • 自平衡: 插入和删除后通过分裂 (Split)合并 (Merge) 保持树平衡
B+ 树结构 (扇出=4) [50, 100] [20] [70] [120] [10, 15, 18] [22, 25, 30] [55, 60, 65] [80, 85, 90] [110, 115, 118] 叶子节点链表 (顺序访问) 图:B+ 树内部节点仅作导航,所有数据在叶子节点,链表连接支持范围扫描
图:B+ 树内部节点仅作导航,所有数据在叶子节点,链表连接支持范围扫描

B+ 树与 B 树的对比:

特性B 树B+ 树
数据存储所有节点都存储数据仅叶子节点存储数据
内部节点存储键和数据仅存储键 (更多空间用于扇出)
叶子节点链表有,支持顺序访问
范围查询需多次树遍历叶子链表高效扫描
数据聚集数据分散在树中数据集中在叶子层
▸ SQL · 索引操作与优化
-- 创建索引 CREATE INDEX idx_name ON users(name); -- 查看索引 SHOW INDEX FROM users; -- 分析查询使用索引情况 EXPLAIN SELECT * FROM users WHERE name = '张三'; -- 覆盖索引 (索引包含所有查询字段,无需回表) CREATE INDEX idx_name_age ON users(name, age); SELECT name, age FROM users WHERE name = '张三';

演进 B 树 → B+ 树 → 自适应哈希索引

  • B 树 (1970): 所有节点存储数据,适合随机访问,但范围查询效率低
  • B+ 树 (1973): 数据仅在叶子节点,内部节点高扇出,叶子链表支持范围查询
  • 自适应哈希索引 (AHI): InnoDB 自动为频繁访问的 B+ 树路径建立哈希索引,将 O(log n) 优化为 O(1)
  • LSM 树 (2006): 基于日志结构的合并树,适合写密集场景,如 Cassandra、HBase
"B+ 树的成功在于『磁盘友好性』——高扇出降低树高,叶子链表优化范围查询,分裂合并保持平衡。它是权衡了磁盘 I/O、内存占用和查询性能之后的最优解。"
—— 索引设计哲学

取舍 设计中的权衡

📈 扇出 vs 树高
扇出越大,树高越低,I/O 次数越少。但单页能存储的键数受页大小限制,过大的扇出导致节点分裂更频繁,写性能下降。
⚡ 聚簇 vs 非聚簇
InnoDB 使用聚簇索引(主键和数据在一起),主键查询快但二级索引需回表。MyISAM 使用非聚簇索引,数据独立存储,二级索引直接指向数据。
🔍 索引 vs 全表扫描
索引查询适用于高选择性的查询(返回行 < 10%)。对于全表扫描,数据库可能选择不使用索引,因为随机 I/O 开销高于顺序 I/O。
04

事务 ACID

背景 为什么需要事务?

在数据库系统中,多个用户可能同时操作同一份数据,系统也可能在任意时刻崩溃。如果没有任何保障机制,可能会出现部分更新(如银行转账,扣款成功但加款失败)、数据不一致(如并发读取未提交的数据)等问题。

第一性原理: 事务的本质是「将一组操作视为一个不可分割的整体」。ACID 定义了事务必须具备的四个属性:原子性 (Atomicity) 确保事务要么全部完成,要么全部回滚;一致性 (Consistency) 确保数据从一种合法状态转为另一种合法状态;隔离性 (Isolation) 确保并发事务互不干扰;持久性 (Durability) 确保提交后数据不会丢失。

原理 原子性 · 一致性 · 隔离性 · 持久性

原子性: 通过 undo 日志 实现。事务执行过程中,所有修改记录到 undo log,如果事务失败,根据 undo log 将数据恢复到修改前的状态。

一致性: 通过数据库约束(主键、外键、唯一约束、触发器)和事务操作的正确性保证。

隔离性: 通过 锁机制MVCC 实现,详见后面章节。

持久性: 通过 redo 日志 实现。事务提交前,所有修改先写入 redo log(顺序写,快于随机写),系统崩溃后通过 redo log 重新应用修改。

ACID 依赖关系 原子性 (A) 全有或全无 undo log 一致性 (C) 合法状态转换 约束 · 触发器 隔离性 (I) 并发控制 锁 · MVCC 持久性 (D) 提交后不丢失 redo log A + I + D → C (一致性是结果) 事务生命周期 BEGIN 操作 1 操作 2 COMMIT 结束 图:ACID 四属性相互配合,确保事务可靠性
图:ACID 四属性相互配合,确保事务可靠性
▸ SQL · 事务示例
-- 银行转账事务: 原子性保证 A 扣款和 B 加款同时成功或同时失败 START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE account_id = 1001; UPDATE accounts SET balance = balance + 100 WHERE account_id = 1002; -- 如果两步都成功,提交事务 (持久化) COMMIT; -- 如果任何一步失败,回滚 (原子性回滚) ROLLBACK;

演进 无事务 → ACID → 分布式事务 (XA) → 最终一致性

  • 无事务 (早期数据库): 无 ACID 保证,数据易损坏,只能单用户访问
  • ACID (1970s): 引入事务概念,保障数据可靠性和一致性
  • XA 分布式事务 (1990s): 通过两阶段提交 (2PC) 实现跨数据库事务
  • 最终一致性 (2000s): NoSQL 放弃强 ACID,用 BASE (基本可用、软状态、最终一致性) 换取更高可用性和扩展性
  • NewSQL (2010s): 在分布式系统中恢复 ACID,支持强一致性
"ACID 是数据库可靠性的基石,但它与『分布式扩展』之间存在矛盾。传统 ACID 要求强一致性,而分布式系统需要 CAP 中的可用性和分区容忍性。NewSQL 通过新架构解决了这个矛盾。"
—— 事务设计哲学

取舍 设计中的权衡

⚡ 隔离性 vs 并发性能
高隔离级别(可串行化)保证最强一致性,但大大降低并发性能。实际系统在隔离性和性能之间选择折中方案(如可重复读)。
📈 事务粒度 vs 锁竞争
大事务减少锁切换开销,但增加锁竞争和死锁概率。小事务提高并发度,但增加事务管理开销。需要根据业务特点权衡。
🔒 持久性 vs 写性能
redo log 保证持久性,但每次提交都要刷盘。可以选择设置 innodb_flush_log_at_trx_commit=2 来牺牲部分持久性换取写性能。
05

四种隔离级别

背景 并发事务会导致哪些问题?

当多个事务并发执行时,如果没有适当的隔离措施,会出现三种常见异常:

  • 脏读 (Dirty Read): 事务 A 读取了事务 B 未提交的修改,若 B 回滚,A 读到了无效数据
  • 不可重复读 (Non-repeatable Read): 事务 A 两次读取同一数据,期间事务 B 修改并提交,A 两次读到的结果不一致
  • 幻读 (Phantom Read): 事务 A 两次查询同一范围,期间事务 B 插入/删除了符合条件的数据,A 两次看到的结果集不一致
第一性原理: 隔离级别的本质是「在并发性能和数据一致性之间划定边界」。从最低到最高,隔离级别逐步解决上述问题,但同时并发性能随之降低。事务隔离级别不是一个非此即彼的选择,而是一个光谱,用户根据自己的业务需求选择适当的平衡点。

原理 读未提交 · 读已提交 · 可重复读 · 可串行化

SQL 标准定义了四种隔离级别,对应解决的问题不同:

隔离级别脏读不可重复读幻读实现机制
读未提交 (Read Uncommitted)⚠️ 可能⚠️ 可能⚠️ 可能无锁,直接读
读已提交 (Read Committed)✅ 避免⚠️ 可能⚠️ 可能MVCC 快照读(普通SELECT不加锁),每次SELECT生成新ReadView
可重复读 (Repeatable Read)✅ 避免✅ 避免⚠️ 可能MVCC 快照读,整个事务首次SELECT生成ReadView并复用
可串行化 (Serializable)✅ 避免✅ 避免✅ 避免严格两阶段锁 (2PL)
隔离级别与并发异常 读未提交 脏读 ⚠️ 不可重复读 ⚠️ 幻读 ⚠️ 读已提交 ⚠️ ⚠️ 可重复读 ⚠️ 可串行化 性能趋势: 高 → 低 高并发 高安全 图:隔离级别越高,并发异常越少,但并发性能越低
图:隔离级别越高,并发异常越少,但并发性能越低

MySQL/InnoDB 默认隔离级别: 可重复读 (Repeatable Read)。RC和RR都使用MVCC快照读,普通SELECT不加锁。两者区别在于ReadView生成时机:RC每次SELECT都生成新ReadViewRR整个事务只在首次SELECT生成一次ReadView并复用——这是可重复读的根因。幻读解决需区分:快照读靠MVCC,当前读(如SELECT FOR UPDATE)靠Next-Key Lock

▸ SQL · 设置隔离级别
-- 查看当前事务隔离级别 SELECT @@transaction_isolation; -- 设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 演示脏读 (在 READ UNCOMMITTED 级别下) -- 事务 A SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; UPDATE accounts SET balance = 1000 WHERE id = 1; -- 事务 B SELECT balance FROM accounts WHERE id = 1; -- 读到 1000 (脏读!) -- 事务 A 回滚 ROLLBACK; -- 事务 B 再次读,变成 100 (数据无效)

演进 无隔离 → SQL 标准 → InnoDB 扩展

  • 无隔离 (早期数据库): 事务完全串行执行,保证一致性但并发度极低
  • SQL 标准隔离级别 (1992): 定义四种级别,规范事务隔离行为
  • MVCC 实现 (InnoDB): 使用多版本并发控制,在可重复读下避免脏读、不可重复读
  • Next-Key Locking: InnoDB 在可重复读级别下使用间隙锁 + 行锁,部分解决幻读
  • 可串行化快照隔离 (SSI): 基于快照的可串行化,如 PostgreSQL 支持
"MVCC 让可重复读成为实际生产中最高效的隔离级别——既保证了并发性能,又避免了脏读和不可重复读。InnoDB 通过 Next-Key Locking 进一步缓解了幻读,使得可重复读接近可串行化的安全性,但保留了更高的并发度。"
—— 隔离级别设计哲学

取舍 设计中的权衡

🔒 锁开销 vs 并发度
高隔离级别(可串行化)需要更多锁,锁竞争激烈,并发度低。低隔离级别(读未提交)锁开销小,但数据一致性风险大。
📈 MVCC 版本管理
MVCC 维护多版本数据,读操作不阻塞写操作,但会增加存储开销和清理成本。长事务会导致版本链过长,影响性能。
🔧 应用层补偿
可重复读下仍有幻读风险,应用层可以通过 SELECT ... FOR UPDATE 或重试机制补偿,但会增加复杂性。
06

锁机制

背景 如何实现并发控制?

数据库需要支持多个事务同时执行,但又要保证数据一致性。锁机制是并发控制的基础手段,通过让事务在访问数据前加锁,确保同一数据在同一时刻只能被一个事务修改。

第一性原理: 锁的本质是「排他性访问控制」。事务在读取数据时加共享锁 (S 锁),多个事务可同时持有;事务在修改数据时加排他锁 (X 锁),同一时刻只能有一个事务持有。锁机制通过「加锁 → 操作 → 解锁」的序列化访问,保证了数据的一致性,但牺牲了并发性能。

原理 S 锁 · X 锁 · 意向锁 · 间隙锁 · 临键锁

锁类型:

  • 共享锁 (S 锁): 允许事务读取数据,多个事务可以同时持有 S 锁
  • 排他锁 (X 锁): 允许事务修改数据,同一时刻只能一个事务持有 X 锁
  • 意向锁 (IS/IX): 表级锁,用于指示事务将在表内某行加锁,避免全表锁冲突
  • 间隙锁 (Gap Lock): 锁定一个区间,防止幻读(InnoDB 可重复读级别下的幻读解决方案)
  • 临键锁 (Next-Key Lock): 行锁 + 间隙锁的组合,锁定行和其之前的间隙
锁兼容性矩阵 S 锁 X 锁 IS 锁 IX 锁 ✅ 兼容 ❌ 冲突 ✅ 兼容 ✅ 兼容 ❌ 冲突 ❌ 冲突 ❌ 冲突 ✅ 兼容 ✅ 兼容 ❌ 冲突 ✅ 兼容 ✅ 兼容 图:锁兼容性矩阵 — S 与 S 兼容,S 与 X 冲突,X 与 X 冲突
图:锁兼容性矩阵 — S 与 S 兼容,S 与 X 冲突,X 与 X 冲突

死锁 (Deadlock): 两个事务互相持有对方需要的锁,导致互相等待。InnoDB 通过超时等待死锁检测机制解决:

  • 默认 50 秒等待超时,超时后回滚一个事务
  • 死锁检测每隔一段时间运行,发现有死锁立即回滚代价最小的事务
▸ SQL · 锁操作与死锁演示
-- 查看当前锁信息 SHOW ENGINE INNODB STATUS\G -- 手动加锁 START TRANSACTION; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加 X 锁 -- 设置锁超时时间 SET SESSION innodb_lock_wait_timeout = 10; -- 10 秒 -- 死锁示例 (两个事务) -- 事务 A START TRANSACTION; UPDATE accounts SET balance = 200 WHERE id = 1; UPDATE accounts SET balance = 300 WHERE id = 2; -- 事务 B START TRANSACTION; UPDATE accounts SET balance = 400 WHERE id = 2; UPDATE accounts SET balance = 500 WHERE id = 1; -- InnoDB 会检测到死锁,回滚其中一个事务

演进 表级锁 → 行级锁 → 临键锁

  • 表级锁 (MyISAM): 锁定整张表,读写互斥,并发度低
  • 行级锁 (InnoDB): 锁定单行,读写可并发,提高并发度
  • 意向锁: 解决表锁和行锁的兼容性问题,提高锁效率
  • 间隙锁 + 临键锁: InnoDB 在可重复读级别下使用,解决幻读问题
  • 自适应锁 (Adaptive Locking): 根据负载动态调整锁策略,如热点数据从行锁升级为表锁
"锁的粒度从表到行再到间隙,体现了『细粒度锁提升并发度』的设计趋势。但细粒度锁也带来了更高的开销和更复杂的管理。InnoDB 在行锁基础上增加间隙锁,在不降低并发度的同时解决了幻读问题。"
—— 锁机制设计哲学

取舍 设计中的权衡

🔒 锁粒度 vs 并发度
表级锁简单高效,但并发度低;行级锁并发度高,但锁管理开销大。InnoDB 使用行锁作为默认,在热点数据上可自适应升级。
⚡ 死锁 vs 性能
死锁检测增加系统开销,但可以及时解除死锁。设置较短的锁超时可以减少死锁等待,但可能导致事务频繁回滚。
📈 间隙锁 vs 幻读
间隙锁防止幻读,但会锁定不存在的记录区间,可能阻塞其他插入操作。在低隔离级别下禁用间隙锁可提升插入性能。
07

MVCC 实现原理

背景 读不阻塞写,写不阻塞读,可能吗?

在传统锁机制中,读操作和写操作互斥:读操作需要加 S 锁,写操作需要加 X 锁,二者不能共存。这导致并发性能受限。MVCC (Multi-Version Concurrency Control) 的设计目标是:读操作不阻塞写操作,写操作也不阻塞读操作,从而实现更高的并发度。

第一性原理: MVCC 的核心思想是「每个写操作都生成一个新版本的数据,而不是覆盖旧版本」。每个事务在开始时获得一个一致性视图 (Consistent View),该视图确定了该事务可以看到哪些版本的数据。读操作直接读取视图中的版本,无需加锁;写操作生成新版本,不影响正在进行的读操作。这种「多版本共存」的设计彻底解决了读写锁冲突问题。

原理 版本链 · 事务 ID · 一致性视图 · 快照读

版本链 (Version Chain): InnoDB 在行数据中隐藏两个列:

  • DB_TRX_ID:最近修改该行的事务 ID
  • DB_ROLL_PTR:指向 undo log 中旧版本的指针,构成版本链
MVCC 版本链与一致性视图 版本链 (Version Chain) 最新版本 (V3) trx_id=103, balance=300 旧版本 (V2) trx_id=102, balance=200 旧版本 (V1) trx_id=101, balance=100 一致性视图 (Consistent View) 事务 ID 范围: [101, 105] 可见版本: V1 (trx_id=101) → 旧版本可见 图:MVCC 版本链通过 DB_ROLL_PTR 串联,一致性视图决定事务可见的版本
图:MVCC 版本链通过 DB_ROLL_PTR 串联,一致性视图决定事务可见的版本

一致性视图 (Consistent View): 事务开始时,系统分配一个 Read View,包含:

  • low_limit_id:当前活跃的最小事务 ID
  • high_limit_id:下一个事务 ID
  • active_ids:当前活跃的事务 ID 列表

可见性规则: 对于一行数据,其 DB_TRX_ID 与视图比较:

  • 如果 DB_TRX_ID < low_limit_id,已提交,可见
  • 如果 DB_TRX_ID >= high_limit_id,未提交,不可见
  • 如果 DB_TRX_IDactive_ids 中,未提交,不可见
  • 否则,已提交,可见
▸ SQL · MVCC 可见性演示
-- 事务 A (读事务,使用一致性视图) START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- 读到的是事务 A 开始时的快照版本 -- 事务 B (写事务,修改数据) START TRANSACTION; UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT; -- 事务 A 再次查询 (仍读旧版本,可重复读) SELECT balance FROM accounts WHERE id = 1; -- 结果与第一次一致,未读到 B 的修改 -- 如果事务 A 执行 SELECT ... FOR UPDATE SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 加锁读,会读到最新的已提交版本 (500)

演进 加锁读 → 快照读 → 可重复读 MVCC

  • 加锁读 (Locking Reads): 传统实现,读操作加 S 锁,写操作加 X 锁,读写互斥
  • 快照读 (Snapshot Read): 通过 MVCC 实现,读操作不加锁,基于一致性视图
  • 可重复读 MVCC: InnoDB 在可重复读级别下,快照读使用一致性视图,实现可重复读
  • 当前读 (Current Read): 通过 SELECT ... FOR UPDATELOCK IN SHARE MODE,读最新已提交版本
"MVCC 是 InnoDB 实现『高并发』的核心武器。它让读操作彻底摆脱了锁的束缚,使得读写可以并发执行,极大提升了系统吞吐量。MVCC 的可重复读实现,本质上是用『版本管理』的代价换取了『读不阻塞写』的收益。"
—— 并发控制设计哲学

取舍 设计中的权衡

📦 版本存储 vs 空间
MVCC 维护多版本数据,每个更新都会产生新版本,增加存储开销。需要定期清理不再需要的旧版本(purge 机制),增加后台 I/O 负担。
⏱ 长事务 vs 版本链长度
长事务会阻止旧版本被清理,导致版本链过长,影响查询性能。需要监控长事务并尽早提交,避免版本堆积。
🔍 快照读 vs 当前读
快照读提供一致性视图但读不到最新数据;当前读能读到最新但需要加锁。需要在业务场景中正确选择读类型,平衡一致性和性能。
08

redo / undo / binlog

背景 数据库崩溃后如何恢复?

数据库系统在运行时可能随时崩溃(断电、进程崩溃、操作系统故障),此时内存中的数据可能还未刷入磁盘。如果不做任何保护,提交的数据可能丢失,未提交的数据可能部分写入。

第一性原理: 数据库使用三种日志实现「崩溃恢复」「事务回滚」redo log 用于已提交数据的重做,保证持久性;undo log 用于未提交数据的回滚,保证原子性和 MVCC;binlog 用于主从复制和时间点恢复,记录逻辑变更。这三种日志各司其职,共同构成了 MySQL 的可靠性体系。

原理 redo log · undo log · binlog

redo log (重做日志): 物理日志,记录页级别的修改。事务提交前,先写入 redo log,保证即使内存数据未刷盘,系统崩溃后也可通过 redo log 重做修改。redo log 的写入是顺序写,性能远高于随机写。

undo log (撤销日志): 逻辑日志,记录修改前的数据。用于事务回滚和 MVCC 版本管理。每条 undo 记录包含事务 ID 和修改前的值。

binlog (二进制日志): 逻辑日志,记录SQL 语句的修改操作。用于主从复制、时间点恢复和审计。binlog 在存储引擎之上,在事务提交后写入。

三种日志的协同工作 事务执行流程 1. 修改内存数据 写入 Buffer Pool 2. 写入 redo log 顺序写 (快速) 3. 写入 undo log 记录旧值 (回滚) 4. 提交 COMMIT redo log 物理日志 · 页级修改 保证持久性 (崩溃恢复) undo log 逻辑日志 · 修改前数据 保证原子性 · MVCC binlog 逻辑日志 · SQL 语句 主从复制 · 时间点恢复 图:三种日志的写入顺序与职责分工
图:三种日志的写入顺序与职责分工

崩溃恢复流程:

  1. 从 redo log 中扫描最后一个检查点 (checkpoint) 之后的日志
  2. 重新应用 redo log,恢复已提交但未刷盘的数据
  3. 扫描 undo log,回滚未提交的事务
▸ Shell · 日志管理与配置
# 查看 redo log 配置 SHOW GLOBAL VARIABLES LIKE 'innodb_log%'; # 查看 binlog 配置 SHOW GLOBAL VARIABLES LIKE 'log_bin%'; # 查看当前 binlog 文件列表 SHOW BINARY LOGS; # 刷新 binlog (产生新文件) FLUSH LOGS; # 从 binlog 恢复数据 mysqlbinlog /var/lib/mysql/mysql-bin.000001 | mysql -u root -p # 查看 redo log 使用情况 SHOW ENGINE INNODB STATUS\G # 输出中包含: Log sequence number, Log flushed up to 等

演进 无日志 → redo/undo → 组提交 → 并行复制

  • 无日志 (早期数据库): 崩溃后数据丢失,需要手动恢复
  • redo/undo 日志 (InnoDB): 实现崩溃恢复和事务回滚,保障 ACID
  • 组提交 (Group Commit): 多个事务的 redo log 批量刷盘,减少 I/O 次数,提升写性能
  • 并行复制 (MTS): 从库并行应用 binlog,提高主从复制性能
  • binlog 压缩 (MySQL 8.0): 对 binlog 进行压缩,减少存储和网络传输开销
"三种日志的设计是 MySQL 可靠性体系的基石。redo log 保障持久性,undo log 保障原子性和 MVCC,binlog 保障复制和恢复。它们相互配合,各司其职,构成了数据库从崩溃到恢复的完整闭环。"
—— 日志设计哲学

取舍 设计中的权衡

📝 顺序写 vs 随机写
redo log 是顺序写,性能优于随机写。但刷盘频率影响持久性:每次提交都刷盘保证持久性,但降低性能;延迟刷盘提高性能但增加丢失风险。
🔁 binlog 格式
binlog 支持 statement(逻辑 SQL)、row(行级别变更)和 mixed 三种格式。statement 日志小但不安全,row 安全但日志量大,mixed 折中。
⚡ 双一配置
innodb_flush_log_at_trx_commit=1 + sync_binlog=1 保证最高可靠性,但写性能最差。根据业务需求可适当调低,在可靠性和性能之间平衡。