阅读时间:约 45 分钟 | 难度:中高级 | 编号:119
一、引言:数据库最核心的承诺
事务是关系型数据库区别于 NoSQL 的最核心特性。ACID 四个字母代表了数据库对数据一致性的四条承诺:
- A(Atomicity)原子性:事务要么全部成功,要么全部失败
- C(Consistency)一致性:事务前后数据满足所有约束
- I(Isolation)隔离性:并发事务之间互不干扰
- D(Durability)持久性:提交的事务永久保存,即使宕机也不丢失
InnoDB 通过 redo log、undo log、锁机制和刷盘策略来实现这四个承诺。本文逐一剖析每种机制的工作原理。

二、redo log:持久性的保障
2.1 为什么需要 redo log?
假设没有 redo log,每次事务提交都需要将修改的数据页刷到磁盘。但数据页是随机分布的,每次刷盘都是随机 I/O,性能极差。
redo log 的核心思想——WAL(Write-Ahead Log):
- 事务修改时,先写 redo log(顺序写,极快)
- redo log 记录了"做了什么修改"
- 事务提交时,只需确保 redo log 落盘即可
- 数据页的修改在后台异步刷盘(Checkpoint)
- 宕机恢复时,重放 redo log 中的记录
2.2 redo log 的存储结构
redo log 文件: ib_logfile0, ib_logfile1
┌──────────────────────────────────────┐
│ LSN 增长方向 → │
│ ┌─────────┬──────────┬────────────┐ │
│ │ 已刷盘 │ 待刷盘 │ 空闲 │ │
│ │(Checkpoint)│(Flush) │ │ │
│ └─────────┴──────────┴────────────┘ │
│ ↑ ↑ │
│ Checkpoint Write Pos │
└──────────────────────────────────────┘
关键参数:
| 参数 | 说明 | 建议 |
|---|---|---|
| innodb_log_file_size | 单个 redo log 文件大小 | 1-4GB |
| innodb_log_files_in_group | redo log 文件数量 | 2-4 |
| innodb_flush_log_at_trx_commit | 刷盘策略 | 1(最安全) |
2.3 innodb_flush_log_at_trx_commit
| 值 | 行为 | 持久性 | 性能 |
|---|---|---|---|
| 0 | 每秒刷一次 | 可能丢失 1 秒数据 | 最高 |
| 1 | 每次提交都刷盘 | 不丢失 | 最低 |
| 2 | 每次提交写 OS 缓存,每秒刷盘 | 宕机不丢,断电可能丢 1 秒 | 中等 |

三、undo log:原子性与 MVCC 的基础
3.1 undo log 的作用
undo log 有两个核心作用:
- 事务回滚:将修改前的数据记录在 undo log 中,回滚时反向操作
- MVCC:读取旧版本数据时,通过 undo log 找到对应版本
3.2 undo log 的存储
INSERT 操作:undo log 只记录主键(回滚时 DELETE)
UPDATE 操作:undo log 记录修改前的值(回滚时恢复)
DELETE 操作:undo log 记录整行数据(回滚时 INSERT)
3.3 redo log 与 undo log 的关系
事务修改流程:
1. 修改数据页 → 生成 undo log(记录旧值)
2. undo log 的修改也会产生 redo log
3. 数据页的修改产生 redo log
4. 事务提交 → redo log 刷盘
5. 如果回滚 → 用 undo log 恢复数据
redo log 保护的是"物理修改"(页的变更)
undo log 保护的是"逻辑修改"(行的旧值)

四、锁机制:隔离性的实现
4.1 锁的类型
| 锁类型 | 粒度 | 说明 |
|---|---|---|
| 表锁(Table Lock) | 表 | MyISAM 默认,InnoDB 也支持 |
| 行锁(Row Lock) | 行 | InnoDB 默认,基于索引 |
| 间隙锁(Gap Lock) | 索引间隙 | 防止幻读 |
| 临键锁(Next-Key Lock) | 行+间隙 | 行锁+间隙锁的组合 |
| 意向锁(Intention Lock) | 表 | 表级意向,协调行锁和表锁 |
4.2 共享锁与排他锁
| 锁模式 | 简称 | 说明 |
|---|---|---|
| 共享锁 | S 锁 | 允许读,不允许写。SELECT ... LOCK IN SHARE MODE |
| 排他锁 | X 锁 | 不允许读写。SELECT ... FOR UPDATE,INSERT/UPDATE/DELETE 自动加 |
4.3 锁的兼容矩阵
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S | ✅ | ❌ | ✅ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ |

五、隔离级别:锁策略的体现
5.1 四种隔离级别
| 级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 不加锁 |
| READ COMMITTED | ❌ | ✅ | ✅ | MVCC + 记录锁 |
| REPEATABLE READ(默认) | ❌ | ❌ | ❌* | MVCC + 临键锁 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 全表加锁 |
- InnoDB 的 REPEATABLE READ 通过临键锁(Next-Key Lock)解决了幻读。
5.2 脏读、不可重复读、幻读
脏读(Dirty Read):
事务 A 读到事务 B 未提交的数据
不可重复读(Non-Repeatable Read):
事务 A 两次读取同一行,结果不同(事务 B 修改并提交了)
幻读(Phantom Read):
事务 A 两次查询同一范围,行数不同(事务 B 插入了新行)

六、MVCC:无锁读的实现
6.1 MVCC 的原理
Multi-Version Concurrency Control(多版本并发控制):读操作不加锁,通过读取历史版本实现非阻塞读。
每行有两个隐藏列:
| 列 | 大小 | 说明 |
|---|---|---|
| DB_TRX_ID | 6 字节 | 最近修改该行的事务 ID |
| DB_ROLL_PTR | 7 字节 | 指向 undo log 中该行的上一个版本 |
6.2 Read View
Read View 是事务开始时的"快照",包含:
- 当前活跃事务列表
- 最小活跃事务 ID
- 下一个待分配的事务 ID
可见性判断: 如果行的 DB_TRX_ID 小于 Read View 的最小活跃事务 ID,该版本可见。如果 DB_TRX_ID 在活跃事务列表中,该版本不可见,需要通过 DB_ROLL_PTR 找到上一个版本。
6.3 当前读 vs 快照读
| 类型 | 说明 | 示例 |
|---|---|---|
| 快照读 | 读取 MVCC 版本,不加锁 | SELECT |
| 当前读 | 读取最新版本,加锁 | SELECT ... FOR UPDATE、UPDATE、DELETE |

七、刷盘策略:平衡性能与安全
7.1 双写缓冲(Doublewrite Buffer)
问题: InnoDB 的页是 16KB,但 OS 的页通常是 4KB。一个 InnoDB 页的写入可能被拆成 4 次 OS 写入。如果写入过程中宕机,16KB 的页可能只有部分写入成功(页损坏)。
解决方案: Doublewrite Buffer
- 先将脏页顺序写入 Doublewrite Buffer(1MB 连续空间)
- 再将脏页写入实际的表空间位置
- 如果第二步宕机,恢复时从 Doublewrite Buffer 恢复完整页
7.2 自适应刷脏(Adaptive Flushing)
InnoDB 根据 redo log 的生成速度动态调整刷脏页的频率,避免 redo log 写满导致的性能骤降。
7.3 关键参数
| 参数 | 说明 |
|---|---|
| innodb_doublewrite | 是否开启双写缓冲 |
| innodb_max_dirty_pages_pct | Buffer Pool 中脏页的最大比例 |
| innodb_io_capacity | 后台 I/O 操作的吞吐量上限 |

八、总结
ACID 的实现是一个系统工程:
| 特性 | 实现机制 | 关键组件 |
|---|---|---|
| 原子性 | undo log | 回滚段 |
| 一致性 | 约束 + undo log | 主键、外键、NOT NULL |
| 隔离性 | 锁 + MVCC | 行锁、间隙锁、Read View |
| 持久性 | redo log + 刷盘 | Doublewrite Buffer、WAL |

九、面试官视角提问
Q1:**innodb_flush_log_at_trx_commit=2**,MySQL 进程崩溃和操作系统崩溃,分别会丢失多少数据?为什么?
Q2:UPDATE 语句执行过程中,InnoDB 依次生成了哪些日志?redo log 和 undo log 的生成顺序是怎样的?
Q3:REPEATABLE READ 隔离级别下,一个事务执行 **SELECT * FROM t WHERE id BETWEEN 1 AND 10** 两次,第二次结果比第一次多了一行。这可能吗?为什么?
Q4:MySQL 8.0 的 Doublewrite Buffer 有什么改进?与之前的版本有什么不同?
Q5:一个长事务执行了 1 小时还未提交,对数据库有什么影响?从 undo log、锁、redo log 三个角度分析。
Q6:如果 redo log 被写满(Checkpoint 跟不上写入速度),MySQL 会怎么处理?如何避免这种情况?
作者注:本文为系列第 119 篇,属于「08 MySQL 与关系型数据库」分类。
十、面试官提问 · 参考答案
Q1:,MySQL 崩溃和 OS 崩溃分别丢失多少数据?
参考答案:
MySQL 进程崩溃:不丢失数据。 redo log 已写入 OS 缓存,MySQL 重启时从 OS 缓存中恢复。
操作系统崩溃/断电:可能丢失 1 秒的数据。 OS 缓存中的数据未刷到磁盘,最多丢失最近 1 秒内提交的事务。
三种值对比:
| 值 | MySQL 崩溃 | OS 崩溃/断电 | 性能 |
|---|---|---|---|
| 0 | 丢失 1 秒 | 丢失 1 秒 | 最高 |
| 1 | 不丢失 | 不丢失 | 最低 |
| 2 | 不丢失 | 丢失 1 秒 | 中等 |
Q2:UPDATE 执行过程中,redo log 和 undo log 的生成顺序是什么?
参考答案:
顺序:undo log → redo log(undo 的修改)→ 数据页修改 → redo log(数据页的修改)
- 生成 undo log,记录 name='Alice'(旧值)
- undo log 的写入本身也会产生 redo log(undo 页的修改也需要保护)
- 修改数据页,name='Bob'
- 数据页修改产生 redo log
- 事务提交时刷 redo log
redo log 保护一切物理修改,包括 undo log 本身。
Q3:REPEATABLE READ 下,两次 结果不同,可能吗?
参考答案:
在 InnoDB 的 REPEATABLE READ 下,不可能。 InnoDB 通过 Next-Key Lock 解决了幻读。
如果第二次结果多了一行,说明:
- 隔离级别不是 REPEATABLE READ(可能是 READ COMMITTED)
- 第一次查询是快照读,第二次查询是当前读(
SELECT ... FOR UPDATE) - 使用了非 InnoDB 引擎(如 MyISAM 不支持行锁)
验证:SELECT @@transaction_isolation; 确认隔离级别。
Q4:MySQL 8.0 的 Doublewrite Buffer 有什么改进?
参考答案:
MySQL 8.0 将 Doublewrite Buffer 从系统表空间移到了独立的 Doublewrite 文件中。
改进:
- 独立文件,减少系统表空间的竞争
- 支持多个 Doublewrite 文件,分布在不同的存储设备上
- 支持动态配置 Doublewrite 文件的存放位置
配置:
-- MySQL 8.0.20+
-- 查看 Doublewrite 状态
SHOW STATUS LIKE '%doublewrite%';
-- Doublewrite 文件位于数据目录下
-- #ib_16384_0.dblwr, #ib_16384_1.dblwr
Q5:长事务执行 1 小时未提交,从 undo log、锁、redo log 三个角度分析影响?
参考答案:
undo log 影响: 事务修改的数据,其旧版本保留在 undo log 中。其他事务的 MVCC 快照读需要读取这些旧版本。undo log 无法被 purge 清理,导致 undo 表空间膨胀。
锁影响: 事务持有的行锁、间隙锁一直不释放,阻塞其他事务的写入操作。可能导致锁等待超时或死锁。
redo log 影响: 事务产生的 redo log 无法被 Checkpoint 覆盖(因为事务未提交,redo log 需要保留用于崩溃恢复)。如果 redo log 写满,数据库会暂停所有写入。
排查:
-- 查找长事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 3600;
Q6:redo log 写满时 MySQL 怎么处理?如何避免?
参考答案:
写满时 MySQL 会暂停所有写入操作,等待 Checkpoint 推进,将脏页刷到磁盘,释放 redo log 空间。表现为数据库突然"卡住"。
避免方法:
- 增大 redo log 文件大小(
innodb_log_file_size) - 增加 redo log 文件数量(
innodb_log_files_in_group) - 提高刷脏页速度(
innodb_io_capacity、innodb_max_dirty_pages_pct) - 避免长事务(长事务阻止 Checkpoint 推进)
- 监控 redo log 使用率:
SHOW STATUS LIKE 'Innodb_log_waits';
-- 如果 > 0,说明 redo log 写满过
全文完 | 编号 119 | 分类 08 MySQL 与关系型数据库