MySQL / InnoDB 存储引擎系列 · 第四篇
为什么强行杀掉数据库进程,已提交的数据也不会丢?
上一篇讲到:脏页不会立刻刷盘,后台慢慢刷就行,前提是"只要日志写盘了,崩溃后能靠日志把它重放回来"。这一篇把这句话坐实——不是讲道理,是真的把本机运行的 MySQL 进程 kill -9 掉,看已提交和未提交的数据分别会怎样。
这就是 redo log:先写日志、后写数据(Write-Ahead Logging)。下面所有现象都是本机 MySQL 9.7.1 真实崩溃、真实重启、真实查出来的结果。
kill -9
两次真实杀死 mysqld 进程做的实验
已提交 → 在
进程被杀后重启,已 COMMIT 的行原样还在
未提交 → 消失
同样被杀,未 COMMIT 的行重启后不见了
Write-Ahead Logging:先写日志,后写数据
核心原则一句话:任何修改要生效,必须先把"打算怎么改"写进日志(并且日志已经落盘),然后才谈得上"这个修改是安全的"——至于对应的数据页什么时候真正刷盘,上一篇已经说过,不着急。
LSN
日志序列号
单调递增的计数器,标记日志流写到哪了。每个页也记录着"最后一条应用到它身上的日志是第几号"——这是判断"这个页新不新"的依据。
mtr
mini-transaction
InnoDB 内部最小的原子日志单元——不是 SQL 那个"事务",而是"对页做的一小组修改,要么全记下来,要么都不记"。源码文件名直译就是"迷你事务缓冲区"。
崩溃恢复
先重放,再回滚
重启时分两步:先把日志里已经落盘的记录全部重放一遍(不管是谁的事务),再把那些"重放出来但没有提交记录"的事务,依据 undo log 撤销掉。
storage/innobase/trx/trx0roll.cc · mysql-server @ mysql-9.7.1, L708 —— 这就是下面实验里"未提交的行消失了"背后真正执行的函数
/** Rollback or clean up any incomplete transactions which were
encountered in crash recovery. If the transaction already was
committed, then we clean up a possible insert undo log. If the
transaction was not yet committed, then we roll it back. */
void trx_rollback_or_clean_recovered(bool all)
真实实验:两次 kill -9
不是模拟——直接找到本机 mysqld 的真实进程号,发 SIGKILL,进程立刻消失,没有任何优雅关闭的机会。
实验 A:提交之后立刻杀
真实实测
$ mysql -uroot redodemo -e "START TRANSACTION; INSERT INTO ledger VALUES (1,'committed_before_crash'); COMMIT;"
# 命令成功返回之后……
$ PID=$(cat /opt/homebrew/var/mysql/*.pid); kill -9 $PID
进程立即消失,没有任何关闭日志
$ mysql.server start
2026-08-27T14:06:47 [System] Starting XA crash recovery...
2026-08-27T14:06:47 [System] XA crash recovery finished.
$ mysql -uroot redodemo -e "SELECT * FROM ledger;"
id marker
1 committed_before_crash ← 还在
实验 B:提交前杀(事务开着一直没 COMMIT)
真实实测
$ mysql -uroot redodemo <<'EOF' & # 后台开一个连接,故意不提交
START TRANSACTION;
INSERT INTO ledger VALUES (2,'UNCOMMITTED_should_vanish');
SELECT SLEEP(20);
EOF
# 这个连接还在 SLEEP,事务还开着……
$ PID=$(cat /opt/homebrew/var/mysql/*.pid); kill -9 $PID
进程立即消失
background: ERROR 2013 (HY000): Lost connection to MySQL server during query
$ mysql.server start && mysql -uroot redodemo -e "SELECT * FROM ledger;"
id marker
1 committed_before_crash
(id=2 那一行,没有了)
这不是巧合,也不是"运气好没丢数据"——这是 WAL 设计出来就该有的行为:已经提交(并且已经 fsync 落盘)的东西,进程再怎么被强杀都丢不了;没提交的东西,哪怕已经改了内存里的页,重启后也会被系统主动撤销,不会留下一半的痕迹。
时间线复盘
事务 A INSERT id=1 → 页在内存里被改了(这就是脏页,上一篇的话题)
事务 A COMMIT → innodb_flush_log_at_trx_commit=1(默认值),这一步会同步等日志 fsync 完成才返回
事务 B INSERT id=2 → 页在内存里也被改了,日志也生成了,但事务还没走到 COMMIT
💥 kill -9:内存里的一切(缓冲池、未提交状态)瞬间消失;唯独已经落盘的日志文件还在
重启·阶段1 重放(redo)磁盘上日志里记录的每一次页修改——不分是谁的事务,id=1 和 id=2 都会先被"重放"回来
重启·阶段2 扫描重放出来的事务:A 有提交记录,放行;B 没有提交记录,用 undo log 撤销——id=2 被拿掉
关键细节:id=2 的修改确实被重放过一次,不是"日志里压根没有它所以没重放"。真正让它消失的,是重放之后那道单独的"清理未提交事务"关卡。这也是为什么崩溃恢复要分两个阶段,而不是一步到位——重放阶段只管"页对不对得上日志",完全不关心事务提交与否;提交与否是第二阶段才处理的问题。
演示:重放,再回滚
用上面真实实验的结构做了一个可验证的最小模型(两个事务、两条日志记录),重放规则和真实源码里的两阶段完全一致。
innodb_flush_log_at_trx_commit:durability 是可以调的
上面的实验用的是默认值 1——每次 COMMIT 都同步等 fsync 完成。这不是唯一选项。
| 值 | COMMIT 时做什么 | 能扛住什么 |
| 1(默认,本文实验用的值) | 写日志 + 立即 fsync,等落盘才返回 | mysqld 崩溃、操作系统崩溃、断电——一个都不丢 |
| 2 | 写日志到操作系统缓存,不等 fsync;由后台每秒左右 fsync 一次 | mysqld 崩溃没事(数据在 OS 缓存里);但操作系统崩溃或断电,最近这一小段可能丢 |
| 0 | 连写到操作系统缓存都不是每次 COMMIT 就做,后台每秒左右统一处理 | mysqld 崩溃就可能丢最近一小段——durability 最弱,换来最少的日志相关开销 |
换句话说,=1 买的是"进程崩、系统崩、断电,三种情况都不丢"的保证,代价是每次提交都要等一次磁盘 fsync;调到 0 或 2 是在拿"最坏情况下丢一小段最近的提交"去换更高的吞吐——这是一个诚实的权衡,不是免费的性能提升。本文的两次真实实验都是在默认值 1 下做的,这也是为什么"已提交必然不丢"这句话在这里成立。
真实机器上的 LSN
本机空闲时刻,三个 LSN 位点是相等的——因为没有新的修改在发生。
SELECT * FROM performance_schema.global_status WHERE VARIABLE_NAME LIKE 'Innodb_redo_log%';
Innodb_redo_log_checkpoint_lsn 571476695
Innodb_redo_log_current_lsn 571476695
Innodb_redo_log_flushed_to_disk_lsn 571476695
三者的关系永远是 checkpoint_lsn ≤ flushed_to_disk_lsn ≤ current_lsn:current_lsn 是日志流内存里写到哪了;flushed_to_disk_lsn 是真正落盘到哪了——这条线以下的修改,进程崩了也丢不了,就是本文实验验证的那条线;checkpoint_lsn 更保守,它要求对应的数据页本身也已经刷盘(上一篇的话题),只有这条线以下的日志空间才能被安全地循环复用。三条线不重合的时候,差的就是"日志已经安全但页还没刷"和"日志还没来得及落盘"这两段窗口。
参考与说明
- "两次 kill -9"实验是本文的核心证据,不是复述文档——两次都在本机真实的 MySQL 9.7.1 进程上做的,终端输出、错误日志时间戳、重启后的查询结果均为真实记录,未做任何删改(只是截取了相关行)。
- 源码引用(trx0roll.cc 的 trx_rollback_or_clean_recovered)取自 mysql/mysql-server 仓库 mysql-9.7.1 标签(commit a26ea1a2),与本机安装的 MySQL 9.7.1 完全一致。
- 演示动画里"后台日志写入线程会独立于 COMMIT 主动 flush"这一细节是真实存在的机制(否则"回滚未提交事务"这个恢复阶段就没有存在的必要),但具体触发时机(缓冲区写满、周期性 flush 等)属于简化叙述,没有在源码层面逐条核对触发条件。
- 没有涉及:undo log 的物理结构、binlog 与 redo log 的两阶段提交(XA,本文错误日志片段里出现的"XA crash recovery"字样正是这个机制的一部分)——前者是系列第五篇的内容。
→