MySQL 原理图解 · 内核与运维

第二辑。日志、内存、事务异常、执行计划、备份恢复——这几块面试问得最深,也最依赖图才能说清。

redo / undo / binlog:三种日志各在哪一层

这题几乎必问,而且九成人第一次答都会把 binlog 说成「InnoDB 的日志」。先看它们的位置——位置决定了归属,归属决定了用途。

SERVER 层(跟用什么引擎无关)
binlog 二进制日志
逻辑日志:记「改了什么」
追加写,不循环
主从复制、按时间点恢复
↓ handler API
INNODB 引擎层
Buffer Pool
数据页缓存
redo log 重做日志
物理日志:记「某页某偏移改了什么」
循环写,固定大小
崩溃恢复(持久性)
undo log 回滚日志
逻辑日志:记「反向操作」
存在 undo 表空间
事务回滚 + MVCC 版本链
当前高亮三种日志全部
归属—
解决什么—
对比项redo logundo logbinlog
归属InnoDB 引擎层InnoDB 引擎层Server 层
日志类型物理日志(页级)逻辑日志(反向 SQL)逻辑日志(语句或行)
写什么「第 3 号表空间第 5 页偏移 128 处改成 X」「把这行的 balance 改回 100」「UPDATE 了 id=1 的行,a 从 1 变 2」
写入方式循环写,大小固定,写满就覆盖随事务增长,purge 后清理追加写,写满一个文件切换下一个
核心作用崩溃恢复 + 保证持久性(WAL)事务回滚 + MVCC 版本链主从复制 + 按时间点恢复
是否可关不能关不能关可以(skip_log_bin),但主从和恢复就没了
一句话串起来:改数据时,redo 记「怎么重做」保证不丢,undo 记「怎么撤回」保证能回滚和快照读,binlog 记「发生了什么」用来同步和恢复。前两个是引擎自己的事,第三个是 Server 层的事——所以换引擎 binlog 还在,主从照跑。
必被追问:redo log 为什么能保证不丢?因为它用 WAL:先写日志再改数据页。innodb_flush_log_at_trx_commit=1 时,事务提交前 redo 必须刷盘。而数据页是脏页,可以慢慢刷——把「随机写 16KB 页」变成「顺序写日志」。这就是 InnoDB 比 MyISAM 抗崩溃的根本原因。

Buffer Pool:三个链表,页在里面来回搬

问「MySQL 读数据是读磁盘吗」,答「不是,先读 Buffer Pool」只对一半。真正要讲的是:页在三个链表之间怎么流动,以及为什么要有 Change Buffer 和 Doublewrite。

三个链表的分工

free list
空闲页
刚启动时所有页都在这
LRU list
缓存页
分 young / old 两段,防全表扫污染
flush list
脏页
改过但还没写回磁盘
change buffer
写缓冲
非唯一二级索引的写,先记账
这一步一切就绪
发生什么—
磁盘 IO无

为什么要分 young 和 old 两段

LRU 链表按 5:3 分成 young(热)和 old(冷)。新读进来的页先插到 old 区头部,只有在 old 区停留超过 innodb_old_blocks_time(默认 1000ms)后再次被访问,才升到 young 区。

这道防线是为全表扫准备的。没有它,一条扫 2000 万行的 SQL 会把所有热页挤出去,缓存命中率一夜归零。有了 old 区,全表扫读进来的页只在冷区转一圈就被淘汰,不污染热数据。

Change Buffer 和 Doublewrite 各解决什么

组件解决什么问题关键限制
Change Buffer非唯一二级索引的写,如果目标页不在内存,先把变更记下来,等页被读进来时再 merge。把随机写变顺序写唯一索引用不了——插入时必须读页校验唯一性,躲不掉
Doublewrite16KB 的页写到一半断电 → 页断裂,redo 也救不了(redo 是物理日志,前提是页完整)。所以先顺序写一份到 doublewrite 区占额外磁盘空间(约 2MB × 2),innodb_doublewrite 可关但不建议
Log Bufferredo 先写内存缓冲,再由 innodb_flush_log_at_trx_commit 决定何时刷盘=1 最安全(每次提交刷);=0 每秒刷(可能丢 1 秒);=2 写 OS cache
一个常被忽略的连锁反应:Change Buffer 的收益取决于「写完多久之后才会读到这个页」。如果写完立刻读(比如刚插入就查),merge 马上发生,等于白记账。所以它在写多读少、且读是延后的场景才划算。

脏读 / 不可重复读 / 幻读:三个现象差在哪

很多人能背定义,但说不出「幻读和不可重复读到底怎么区分」。区别只在一件事:不可重复读是同一行变了,幻读是行数变了。

时间线

T1
1
3
T2
2
4
事件—
异常点—
本质—

三种现象对照

现象场景读到了什么本质
脏读T2 读到 T1 还没提交的修改,T1 回滚了一个从未真正存在过的值读了未提交数据
不可重复读T1 内两次读同一行,中间 T2 改了并提交同一行,两次值不同行内容变了
幻读T1 内两次范围查询,中间 T2 插入并提交行数变多(或变少)行数变了

哪个隔离级别能防住哪个

隔离级别脏读不可重复读幻读
READ UNCOMMITTED会发生会发生会发生
READ COMMITTED防住会发生会发生
REPEATABLE READ(默认)防住防住快照读防住 / 当前读靠间隙锁
SERIALIZABLE防住防住防住
RR 那一格是全场唯一的坑。标准 SQL 定义里 RR 防不住幻读,InnoDB 用两套机制把它补上了:快照读靠 MVCC 的 ReadView(你要的范围在快照里就是固定的),当前读靠 next-key lock(不许往这个区间插)。所以严格说,InnoDB 的 RR 在绝大多数场景下没有幻读——但不是「因为 RR 这个级别本身」。
面试官最爱在这里设套:「你说 RR 能防幻读,那 SELECT ... FOR UPDATE 之后再 SELECT 会怎样?」——前者是当前读、会加间隙锁,后者是快照读、看 ReadView。两条路径不一样,答的时候一定要区分。

EXPLAIN:type 那一列是有等级阶梯的

问「怎么看执行计划」,只答「看有没有走索引」太浅。type 是有明确优劣顺序的,而且相邻两级的性能差可能是数量级的。

type 从好到坏

system› const› eq_ref› ref› range› index› ALL
typeconst
keyPRIMARY
rows1
Extra—
判断—

各等级的含义

type含义典型场景
system表只有一行系统表,实际遇不到
const主键或唯一索引等值,最多一行WHERE id = 10
eq_ref联表时被驱动表用主键/唯一索引匹配JOIN ... ON a.id = b.uid
ref普通索引等值,可能多行WHERE a = 5
range索引范围扫描WHERE a > 5、BETWEEN、IN
index扫整棵索引树(不是扫表,但也不筛)覆盖索引但无过滤条件
ALL全表扫描没索引、或优化器判定走索引更贵
index 这一级最容易被误读。它看着是「用了索引」,实际是把整棵索引从头扫到尾。如果表很大而过滤性又差,它和 ALL 差不多慢,只是扫的东西更小(只扫索引页,不扫数据页)。判断标准永远是 rows 和实际返回行数的比值,不是「有没有 key」。
Extra 里值得记住的几个:
Using index — 覆盖索引,没回表,好。
Using index condition — 索引下推生效,在索引层先过滤。
Using filesort — 额外排序,数据量大就慢。
Using temporary — 用了临时表,通常是 GROUP BY 或 DISTINCT 没走索引。
Using where — 拿到行之后还要过滤,配合大 rows 就是慢的信号。

备份恢复:全备 + binlog 怎么恢复到某一秒

问「误删了数据怎么恢复」,答「用备份恢复」会被追问到底。完整答案是两段拼接:先恢复最近一次全备,再重放 binlog 到事故前一刻。

全备
最后一次可用点
误删
周日 02:00
mysqldump / XtraBackup
binlog 持续累积
(这段必须没被清)
周三 14:32
DROP TABLE
第一步—
第二步—
最终状态—

给出的命令就是这个顺序

# 1. 恢复最近一次全备
mysql < full_20261108.sql

# 2. 从全备位点开始,重放 binlog 到误删前一刻
mysqlbinlog --start-position=154 \
  --stop-datetime="2026-11-12 14:31:59" \
  mysql-bin.000012 mysql-bin.000013 | mysql

# 3. 校验
CHECKSUM TABLE 业务表;
决定成败的不是恢复命令,是两件平时就得做的事:
① binlog 不能被提前清掉。默认 binlog_expire_logs_seconds 是 30 天(8.0 之前叫 expire_logs_days),如果你的全备周期是周备、保留 7 天,那最新一份备份之后如果 binlog 也只剩 7 天,刚好够;要是备份失败过一周,就直接断路了。
② 必须定期做恢复演练。备份文件存在不等于能恢复——版本不一致、字符集不对、GTID 冲突,都会让你在事故现场才发现。
有 GTID 时更省事:不用记位点,直接指定「恢复到哪个 GTID 之前」——mysqlbinlog --include-gtids。这也是为什么现在生产环境基本都开 GTID:故障切换和恢复都不需要人肉算坐标。
物理备份 vs 逻辑备份怎么选:XtraBackup 属物理备份,直接拷数据页,速度快、支持增量,几百 GB 的表也只能用它;mysqldump 属逻辑备份,导出 SQL 文本,可跨版本、可只导单表,但慢,且恢复时要重新建索引。大库用物理、小库用逻辑,这是行业默认做法。