事务 ACID 实现:redo log、undo log、锁和刷盘策略(119)

MySQL与关系型数据库约 9 分钟阅读5,046 字符
知识库文档 120 / 251

阅读时间:约 45 分钟 | 难度:中高级 | 编号:119


一、引言:数据库最核心的承诺

事务是关系型数据库区别于 NoSQL 的最核心特性。ACID 四个字母代表了数据库对数据一致性的四条承诺:

  • A(Atomicity)原子性:事务要么全部成功,要么全部失败
  • C(Consistency)一致性:事务前后数据满足所有约束
  • I(Isolation)隔离性:并发事务之间互不干扰
  • D(Durability)持久性:提交的事务永久保存,即使宕机也不丢失

InnoDB 通过 redo log、undo log、锁机制和刷盘策略来实现这四个承诺。本文逐一剖析每种机制的工作原理。

119_fig1.png
119_fig1.png


二、redo log:持久性的保障

2.1 为什么需要 redo log?

假设没有 redo log,每次事务提交都需要将修改的数据页刷到磁盘。但数据页是随机分布的,每次刷盘都是随机 I/O,性能极差。

redo log 的核心思想——WAL(Write-Ahead Log):

  1. 事务修改时,先写 redo log(顺序写,极快)
  2. redo log 记录了"做了什么修改"
  3. 事务提交时,只需确保 redo log 落盘即可
  4. 数据页的修改在后台异步刷盘(Checkpoint)
  5. 宕机恢复时,重放 redo log 中的记录

2.2 redo log 的存储结构

code
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 秒 中等

119_fig2.png
119_fig2.png


三、undo log:原子性与 MVCC 的基础

3.1 undo log 的作用

undo log 有两个核心作用:

  1. 事务回滚:将修改前的数据记录在 undo log 中,回滚时反向操作
  2. MVCC:读取旧版本数据时,通过 undo log 找到对应版本

3.2 undo log 的存储

code
INSERT 操作:undo log 只记录主键(回滚时 DELETE)
UPDATE 操作:undo log 记录修改前的值(回滚时恢复)
DELETE 操作:undo log 记录整行数据(回滚时 INSERT)

3.3 redo log 与 undo log 的关系

code
事务修改流程:
1. 修改数据页 → 生成 undo log(记录旧值)
2. undo log 的修改也会产生 redo log
3. 数据页的修改产生 redo log
4. 事务提交 → redo log 刷盘
5. 如果回滚 → 用 undo log 恢复数据

redo log 保护的是"物理修改"(页的变更)
undo log 保护的是"逻辑修改"(行的旧值)

119_fig3.png
119_fig3.png


四、锁机制:隔离性的实现

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 ❌ ❌ ❌ ❌

119_fig4.png
119_fig4.png


五、隔离级别:锁策略的体现

5.1 四种隔离级别

级别 脏读 不可重复读 幻读 实现方式
READ UNCOMMITTED ✅ ✅ ✅ 不加锁
READ COMMITTED ❌ ✅ ✅ MVCC + 记录锁
REPEATABLE READ(默认) ❌ ❌ ❌* MVCC + 临键锁
SERIALIZABLE ❌ ❌ ❌ 全表加锁
  • InnoDB 的 REPEATABLE READ 通过临键锁(Next-Key Lock)解决了幻读。

5.2 脏读、不可重复读、幻读

code
脏读(Dirty Read):
事务 A 读到事务 B 未提交的数据

不可重复读(Non-Repeatable Read):
事务 A 两次读取同一行,结果不同(事务 B 修改并提交了)

幻读(Phantom Read):
事务 A 两次查询同一范围,行数不同(事务 B 插入了新行)

119_fig5.png
119_fig5.png


六、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

119_fig6.png
119_fig6.png


七、刷盘策略:平衡性能与安全

7.1 双写缓冲(Doublewrite Buffer)

问题: InnoDB 的页是 16KB,但 OS 的页通常是 4KB。一个 InnoDB 页的写入可能被拆成 4 次 OS 写入。如果写入过程中宕机,16KB 的页可能只有部分写入成功(页损坏)。

解决方案: Doublewrite Buffer

  1. 先将脏页顺序写入 Doublewrite Buffer(1MB 连续空间)
  2. 再将脏页写入实际的表空间位置
  3. 如果第二步宕机,恢复时从 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 操作的吞吐量上限

119_fig7.png
119_fig7.png


八、总结

ACID 的实现是一个系统工程:

特性 实现机制 关键组件
原子性 undo log 回滚段
一致性 约束 + undo log 主键、外键、NOT NULL
隔离性 锁 + MVCC 行锁、间隙锁、Read View
持久性 redo log + 刷盘 Doublewrite Buffer、WAL

119_fig8.png
119_fig8.png


九、面试官视角提问

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(数据页的修改)

  1. 生成 undo log,记录 name='Alice'(旧值)
  2. undo log 的写入本身也会产生 redo log(undo 页的修改也需要保护)
  3. 修改数据页,name='Bob'
  4. 数据页修改产生 redo log
  5. 事务提交时刷 redo log

redo log 保护一切物理修改,包括 undo log 本身。


Q3:REPEATABLE READ 下,两次 结果不同,可能吗?

参考答案:

在 InnoDB 的 REPEATABLE READ 下,不可能。 InnoDB 通过 Next-Key Lock 解决了幻读。

如果第二次结果多了一行,说明:

  1. 隔离级别不是 REPEATABLE READ(可能是 READ COMMITTED)
  2. 第一次查询是快照读,第二次查询是当前读(SELECT ... FOR UPDATE)
  3. 使用了非 InnoDB 引擎(如 MyISAM 不支持行锁)

验证:SELECT @@transaction_isolation; 确认隔离级别。


Q4:MySQL 8.0 的 Doublewrite Buffer 有什么改进?

参考答案:

MySQL 8.0 将 Doublewrite Buffer 从系统表空间移到了独立的 Doublewrite 文件中。

改进:

  1. 独立文件,减少系统表空间的竞争
  2. 支持多个 Doublewrite 文件,分布在不同的存储设备上
  3. 支持动态配置 Doublewrite 文件的存放位置

配置:

sql
-- 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 写满,数据库会暂停所有写入。

排查:

sql
-- 查找长事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 3600;

Q6:redo log 写满时 MySQL 怎么处理?如何避免?

参考答案:

写满时 MySQL 会暂停所有写入操作,等待 Checkpoint 推进,将脏页刷到磁盘,释放 redo log 空间。表现为数据库突然"卡住"。

避免方法:

  1. 增大 redo log 文件大小(innodb_log_file_size)
  2. 增加 redo log 文件数量(innodb_log_files_in_group)
  3. 提高刷脏页速度(innodb_io_capacity、innodb_max_dirty_pages_pct)
  4. 避免长事务(长事务阻止 Checkpoint 推进)
  5. 监控 redo log 使用率:
sql
SHOW STATUS LIKE 'Innodb_log_waits';
-- 如果 > 0,说明 redo log 写满过

全文完 | 编号 119 | 分类 08 MySQL 与关系型数据库