MySQL 核心知识查缺补漏

从 InnoDB、索引优化、MVCC、锁到日志与主从复制

Posted by Ekko on October 23, 2025

这篇笔记的目标,是把零散的 MySQL 知识点收束成一条主线:先理解 InnoDB 如何组织数据和索引,再理解查询为什么快或慢,最后把事务、锁、日志和主从复制串起来。

它偏向底层原理和工程判断,不展开执行计划字段逐项说明,也不覆盖具体业务 SQL 的专项调优案例。阅读时重点抓住四件事:数据怎么存、索引怎么查、并发怎么控、数据怎么保住。

参考资料:

MySQL 8.4 Reference Manual

InnoDB Locking and Transaction Model

Semisynchronous Replication

Set Operations with UNION, INTERSECT, and EXCEPT

Point-in-Time Recovery

[TOC]


1. 从 InnoDB 页开始理解 MySQL

MySQL 默认存储引擎是 InnoDB。很多查询、锁和事务现象,最终都要回到 InnoDB 的页结构来理解。

1.1 页是什么

InnoDB 以页(page)作为磁盘和缓存管理的基本单位,默认页大小是 16KB,也可以通过 innodb_page_size 调整为 4KB、8KB、32KB 或 64KB。

可以把页理解为 B+ 树节点在物理层面的主要载体。一个索引树的每个节点,通常就对应一个或多个页;Buffer Pool 里缓存的也是这些页。

1.2 聚簇索引和二级索引分别存什么

在 InnoDB 中,表数据本身按主键组织,主键索引就是聚簇索引。

  • 聚簇索引叶子节点存放完整行记录,核心是“索引即数据”。
  • 二级索引叶子节点存放的是“二级索引列值 + 主键值”,不是直接保存行地址指针。
  • 二级索引查到主键后,再回聚簇索引取完整行,这一步就是常说的回表。

这个细节很重要,因为它直接决定了为什么“二级索引查得快,但不一定一次查完”。

聚簇索引叶子节点内容.jpg

1.3 行很大时会发生什么

单行记录并不是要求必须完整塞进一个页里。对于很长的 BLOBTEXT、较大的 VARCHAR 等列,InnoDB 会使用行溢出机制,把部分数据放到溢出页中,记录里保留指针信息。

需要注意两点:

  • 常见资料会说“超过 8KB 就溢出”,这个说法只能当经验记忆,不能当绝对规则。
  • 是否外部存储,受行格式、字段类型、页大小等因素共同影响;在 DYNAMICCOMPRESSED 行格式下,大字段更容易走外部存储。

2. 为什么索引底层更偏向 B+ 树

2.1 B 树和 B+ 树的关键差异

B 树和 B+ 树都属于多路平衡查找树,但 MySQL 的 InnoDB 选择 B+ 树作为主流索引结构,核心原因有两个:

  • 非叶子节点只存键值和页指针,单页能容纳更多分支,树高更低。
  • 真实数据集中在叶子节点,叶子节点之间还能串成有序链表,范围查询更友好。

B+ 树并不是“层级更高”,恰恰相反,在同样数据量下,它通常更容易降低树高。

2.2 为什么不用二叉树

二叉树每个节点最多两个分支,树高上升太快。数据库更在意随机 I/O 次数,而不是某一次比较多执行了几个 CPU 指令。

2.3 为什么不用 Hash

Hash 结构适合等值查找,但不适合下面这些数据库高频场景:

  • 范围查询,如 >, <, BETWEEN
  • 前缀匹配,如 LIKE 'abc%'
  • 有序遍历,如 ORDER BY

所以数据库通用索引结构更倾向 B+ 树,而不是 Hash。

3. 索引设计要在建表阶段考虑

表结构一旦跑到线上,很多代价会被放大。字段类型、主键设计、索引顺序,都会持续影响 I/O 和锁范围。

3.1 字段长度尽量克制

字段越长,单页能容纳的记录越少,索引也会更胖,进而导致:

  • B+ 树层级更容易增加
  • Buffer Pool 命中率更差
  • 范围扫描时 I/O 次数更多

因此,字段定义要按业务上限设计,而不是习惯性给很大长度。

3.2 主键要稳定、短小、尽量单调

InnoDB 的聚簇索引按主键组织数据,主键会被每个二级索引叶子节点一并保存。主键过长,所有二级索引都会跟着变大。

工程上通常会优先考虑:

  • 短主键
  • 不频繁更新的主键
  • 尽量单调递增的主键

这也是很多业务表偏爱自增主键或趋势递增主键的原因之一。

3.3 怎么看索引区分度

可以用下面这条语句查看索引统计信息:

1
SHOW INDEX FROM table_name;

其中 Cardinality 表示索引基数,是优化器使用的估算值,可以近似理解为“索引列中不同值的数量”。

  • 基数越高,通常区分度越好。
  • 如果基数接近总行数,说明列值比较分散,更适合作为筛选条件。
  • 如果大量记录都重复同一个值,这个索引即便存在,也未必值得优化器使用。

4. 深分页为什么慢

LIMIT offset, size 在偏移量很小时问题不大,但偏移量非常大时,成本会明显上升。

真正的问题不是“先把完整结果集全部查出来”,而是 MySQL 往往需要先扫描并跳过前面很多行,才能返回目标页的数据。如果排序列不能很好利用索引,还可能伴随 filesort、临时表或大量回表。

4.1 典型慢点在哪里

深分页常见慢点有三个:

  • 扫描很多无用记录,只是为了跳过它们
  • 排序无法利用索引,需要额外排序
  • 先通过二级索引定位,再频繁回表取列

4.2 更稳妥的优化思路

第一类优化,是尽量让排序和过滤走到索引上。

第二类优化,是减少回表次数,例如只查需要的列,尽量利用覆盖索引。

第三类优化,是把“按页码翻页”改成“基于上次最后一个键继续查”的 seek 方法,也叫 keyset pagination。

1
2
3
4
5
SELECT id, title
FROM article
WHERE id > 1000000
ORDER BY id
LIMIT 20;

如果业务一定要保留页码语义,常见折中方案是:先用覆盖索引拿到目标页的主键,再回表补齐需要的列。

5. 联合索引、回表、ICP、MRR 要放在一起理解

5.1 联合索引的本质是先按前缀排序

联合索引 (A, B) 的逻辑顺序是:先按 A 排,再在 A 相同的记录里按 B 排。

因此,下面这条语句:

1
WHERE A > 2 AND B = 3

如果索引是 (A, B)

  • A > 2 属于范围条件,优化器可以利用 A 定位到一段索引范围。
  • B = 3 往往不能继续用于“缩小定位范围”。
  • 不过如果 B 也在二级索引里,存储引擎仍可能借助 ICP 在索引层提前过滤掉不满足 B = 3 的记录。

如果索引是 (B, A)

  • B = 3 先做等值定位。
  • B = 3 这一小段范围内,A 仍然有序。
  • 这时 A > 2 可以继续利用索引范围过滤,通常更高效。

这就是“最左前缀原则”在工程上的真实含义:不是背口诀,而是看索引内部是否还能保持有序。

5.2 什么是回表

当查询使用二级索引,但目标列没有被这个索引完全覆盖时,MySQL 会先从二级索引叶子节点拿到主键,再回到聚簇索引读取整行。

这个过程就是回表。

常见优化方式:

  • 只查需要的列,避免 SELECT *
  • 让常用查询尽量命中覆盖索引
  • 高选择性条件优先走索引,减少无效回表

5.3 什么是索引下推

索引下推(ICP, Index Condition Pushdown)的核心思想是:把本来可以在存储引擎层判断的索引列条件,尽量提前到索引扫描阶段执行,而不是先回表、再到 Server 层过滤。

它不能改变联合索引的排序规则,但可以减少不必要的回表。

一个容易记住的判断是:

  • “能不能继续缩小定位范围”看索引有序性。
  • “能不能在回表前先过滤一部分”看 ICP 是否可用。

5.4 什么是 MRR

MRR(Multi-Range Read)主要用于改善二级索引范围扫描后的回表方式。

如果二级索引先拿到一批主键值,直接按拿到的顺序回聚簇索引,I/O 可能很随机。MRR 会把这些主键整理后,再按更接近顺序的方式回表,从而降低随机 I/O 开销。

它常和下面这类场景一起出现:

  • 范围查询
  • 二级索引扫描后大量回表
  • 磁盘型负载或缓存命中不足的场景

6. 索引失效不要背死规则,要看“还能不能用索引做有序定位”

很多资料喜欢把“索引失效”整理成口诀,但更稳妥的理解方式是:优化器是否还愿意使用索引,以及索引还能利用到什么程度。

6.1 常见会削弱索引能力的写法

  • 对索引列做函数或运算,如 WHERE a + 1 = 10
  • 发生隐式类型转换,如字符串列拿数字去比较
  • LIKE '%abc' 这种前导通配符
  • 联合索引没有满足最左前缀
  • 选择性太差,优化器认为全表扫描更便宜

6.2 几个容易被说绝对化的点

1. OR 不等于一定失效

OR 条件两侧如果都能利用索引,优化器可能仍然会选索引合并或其他访问路径。只有当某一侧代价太高、无索引或整体成本不划算时,才更容易退化。

2. !=<>IS NOT NULL 也不是一定失效

这类条件常常选择性不高,所以优化器未必走索引,但并不代表语义上完全不能用索引。

3. UNION ALL 不保证原始顺序

UNION 会去重,UNION ALL 不去重,但二者默认都不保证最终输出顺序。只要业务依赖顺序,就应该在最外层显式写 ORDER BY

6.3 做查询优化时,先看 EXPLAIN 的什么

很多优化并不是靠感觉判断,而是先看优化器准备怎么执行。EXPLAIN 不是答案本身,但它能帮助快速定位大方向。

最常看的几个字段可以这样理解:

  • type:访问方式,通常希望至少达到 rangerefeq_ref 一类,而不是大范围 ALL
  • key:最终实际使用了哪个索引,和 possible_keys 一起看更有意义
  • rows:优化器估计要扫描多少行,数值越大,越值得警惕
  • filtered:扫描后预计还能剩下多少比例的数据
  • Extra:是否出现 Using filesortUsing temporaryUsing indexUsing index condition 等关键信号

工程上经常先问三件事:

  1. 有没有走到预期索引。
  2. 扫描行数是不是明显过大。
  3. 有没有排序、临时表、回表等额外成本。

6.4 覆盖索引、排序、分组为什么经常一起看

很多慢 SQL 不只是“过滤慢”,而是过滤之后的排序和分组没有借上索引。

覆盖索引

如果查询所需列全部包含在索引里,MySQL 就可以直接从索引返回结果,不必回表。

这类查询通常会更稳定,因为它同时减少了:

  • 回表次数
  • 随机 I/O
  • Buffer Pool 压力

ORDER BY

ORDER BY 想走索引,通常要求过滤条件和排序列能匹配到同一条索引访问路径。如果过滤用了一套索引、排序又依赖另一套顺序,就很容易落到 filesort

一个朴素判断是:如果查询本来就沿着某棵 B+ 树按需要的顺序往下扫,排序成本就低;如果先找到结果,再额外重排,成本就会上来。

GROUP BY

GROUP BYORDER BY 类似,也很依赖索引顺序。尤其是高基数字段做分组时,如果前置过滤不够好,临时表和额外排序都可能出现。

因此,很多“查询优化”本质上不是单纯补一个索引,而是同时考虑:

  • 过滤列
  • 排序列
  • 分组列
  • 返回列

6.5 JOIN 优化要先控制驱动表和扫描量

JOIN 慢,很多时候不是连接本身复杂,而是前面某一张表已经扫太多行了。

JOIN 优化时,通常优先关注:

  • 让过滤后结果更小的表尽早参与连接
  • JOIN 条件列建立合适索引
  • 避免在连接列上做函数、计算或隐式类型转换
  • 减少大结果集之后再排序、分组、分页

如果一条 SQL 最终要关联多张表,一个非常实用的思路是:先想办法把“参与连接的数据量”降下来,再谈后面的连接算法和排序成本。

7. 当前读、快照读、Undo Log、MVCC

这一部分是把旧笔记里的内容合并并校正后的版本。

7.1 什么是快照读

在 InnoDB 中,普通 SELECT 在大多数隔离级别下属于一致性非锁定读,也叫快照读。

它的特点是:

  • 读取的是某个一致性视图下可见的数据版本
  • 不会像锁定读那样默认给记录加锁
  • 读写可以并发进行

需要特别纠正一个常见误区:普通 SELECT 不是默认加共享锁。只有 SERIALIZABLE 隔离级别,或者显式使用锁定读语法时,才会引入相应锁行为。

7.2 什么是当前读

当前读可以理解为“读取记录的当前最新版本,并且需要考虑锁冲突”。典型语句包括:

  • SELECT ... FOR UPDATE
  • SELECT ... FOR SHARE
  • UPDATE
  • DELETE

这类语句不仅要读数据,还要参与并发控制,因此通常会加记录锁、间隙锁或临键锁,具体取决于索引条件和隔离级别。

7.3 Undo Log 在这里扮演什么角色

Undo Log 有两个核心作用:

  • 事务回滚
  • 给 MVCC 提供旧版本数据

当记录被修改时,InnoDB 会保留旧版本信息。快照读如果发现当前版本对自己不可见,就会沿着版本链去找一个自己可见的旧版本。

所以更准确的说法不是“快照读每次都在读 undo log”,而是:

  • 能直接读到可见版本时,就直接读当前记录版本。
  • 当前版本不可见时,才会顺着 undo 版本链回溯。

7.4 Read View 是什么

Read View 可以理解为“当前事务看世界的可见性规则”。

它不保存整张表的拷贝,而是保存事务可见性判断所需的信息。借助它,InnoDB 才能判断某个版本对当前事务是否可见。

7.5 RC 和 RR 下 Read View 的差异

Read Committed

每条快照读语句通常都会生成新的 Read View,所以同一个事务里两次普通查询,可能看到不同的提交结果。

Repeatable Read

同一事务中的第一次快照读会建立 Read View,后续快照读通常复用它,因此普通查询结果可重复。

这也是 MySQL 默认隔离级别选择 Repeatable Read 后,很多业务读起来“更稳定”的原因。

8. 四种隔离级别要和读写方式一起看

8.1 Read Uncommitted

  • 可能读到其他事务尚未提交的数据
  • 会出现脏读
  • 在工程实践中几乎很少作为默认选择

8.2 Read Committed

  • 只能看到已提交数据
  • 可以避免脏读
  • 同一事务内重复执行普通查询,可能出现不可重复读
  • 范围读也可能出现幻读

8.3 Repeatable Read

这是 InnoDB 默认隔离级别。

  • 普通快照读在同一事务内通常可重复
  • 锁定读、更新、删除等当前读场景,会配合记录锁、间隙锁、临键锁控制并发
  • 讨论“幻读”时一定要区分快照读和当前读,不要把所有现象混成一句口号

更贴近实现的说法是:

  • 对普通快照读,RR 主要依赖 MVCC 保证可重复读。
  • 对锁定读和范围修改,RR 主要依赖 Next-Key Lock 来降低幻读风险。

8.4 Serializable

最严格的隔离级别,读写冲突最多,并发性最低。只有在业务确实需要强串行语义时才值得考虑。

9. MySQL 锁的重点不是名字,而是“锁住了哪一段索引”

InnoDB 的行锁本质上锁的是索引记录,而不是抽象意义上的“这一行”。

9.1 常见三类锁

Record Lock

锁住具体索引记录。

Gap Lock

锁住索引记录之间的间隙,不包含记录本身,主要用于阻止新的插入。

Next-Key Lock

Record Lock + Gap Lock 的组合,是 InnoDB 在 Repeatable Read 下处理范围并发的重要手段。

9.2 为什么说“没索引会锁很多”

如果语句没有合适索引,MySQL 可能只能扫描大量记录,扫描到哪里就锁到哪里。结果是:

  • 锁范围变大
  • 并发下降
  • 死锁概率上升

所以“建好索引”不仅是为了查询性能,也是为了缩小锁范围。

9.3 唯一索引上的精确命中更容易退化成记录锁

如果使用唯一索引做等值定位,并且精确命中唯一记录,InnoDB 往往只需要加记录锁,不需要把前后间隙一起锁住。

这也是唯一索引在并发控制上经常更干净的原因。

9.4 死锁并不罕见,关键是缩短锁持有时间

只要有并发更新、交叉访问和多条语句组成的事务,死锁就可能出现。死锁不是“数据库坏了”,而是数据库在帮忙做冲突裁决。

更值得关注的是如何降低死锁概率:

  • 事务尽量短,避免长事务占着锁不提交
  • 多个事务按一致顺序访问对象,例如都先改表 A 再改表 B
  • 尽量让条件命中索引,减少“扫描到哪里锁到哪里”
  • 把用户交互、远程调用放在事务外,避免锁跨网络等待

排查时通常先看最近一次死锁信息,再回到 SQL 和索引层面找“锁范围为什么会重叠得这么大”。

10. 插入为什么会碰到自增锁问题

自增主键看起来只是“生成一个新 ID”,但在高并发插入时,背后其实同时有两类问题:

  • 自增值怎么分配,才能保证并发下不重复
  • 事务提交后,变更怎么可靠持久化

这两类问题经常一起出现,但不要混成一件事。

10.1 自增 ID 不重复,不等于“数据安全”

AUTO_INCREMENT 主要解决的是“并发插入时,谁拿到哪个新 ID”。

它能说明 MySQL 在分配自增值时做了并发控制,但不能直接说明数据已经安全落盘。真正和“数据是否保住”相关的,是事务提交时:

  • redo log 是否按配置写入或刷盘
  • binlog 是否按配置写入并与事务提交保持一致
  • 崩溃发生时,事务到底处于已提交还是未提交状态

所以要区分两件事:

  • 自增 ID 不重复,说明分配过程受控
  • 事务提交成功并完成相应持久化,才说明这次写入真正“保住了”

一个非常容易混淆的现象是:ID 已经分配出去,但最终那行数据并没有留下来。例如插入失败、事务回滚、批量插入预留后未全部使用,这些都会造成自增值出现间洞,而且这些值通常不会被重用。

10.2 锁住的更准确说法,是“保护自增计数器的分配过程”

讨论自增锁时,更准确的说法不是“锁住了这几行数据”,而是 InnoDB 需要保护 AUTO_INCREMENT 计数器的分配过程。

在不同模式下,这种保护可能体现为:

  • 特殊的表级 AUTO-INC
  • 更轻量的互斥量(mutex)

这里还有一个很重要的细节:AUTO-INC 锁通常是持有到语句结束,不是持有到事务结束。也就是说,它和普通行锁的观察方式不完全一样,但在并发插入同一张表时,仍然会直接影响吞吐量。

10.3 为什么它会变成性能问题

如果每次插入都要严格串行地分配自增值,那么并发线程越多,等待就越明显。

因此,高并发插入的成本,往往来自两个方向:

  • 前半段是“分配自增值时的竞争”,表现为 AUTO-INC 锁等待或互斥量竞争
  • 后半段是“事务提交时的持久化成本”,表现为 redo log 刷盘、binlog 同步等开销

这也是为什么同样是“插入很慢”,有时瓶颈在锁竞争,有时瓶颈在提交刷盘,二者不能混为一谈。

10.4 innodb_autoinc_lock_mode 决定了并发插入的取舍

MySQL/InnoDB 通过 innodb_autoinc_lock_mode 控制自增值的分配方式。它不是单纯的“开锁或不开锁”,而是要在连续性、并发度、复制安全性之间做权衡。

0:traditional

这是最传统的方式,所有 INSERT-like 语句都会拿表级 AUTO-INC 锁,并通常持有到语句结束。

  • 优点是行为最接近早期版本,结果更可预测
  • 缺点是并发插入更容易串行化,吞吐量最差
  • 它适合强调可重复执行语义、兼容旧行为的场景

1:consecutive

这是一个折中模式,可以把你说的“已知插入量就预分配,不知道就沿用旧策略”更准确地表述出来。

  • 对于 INSERT ... VALUES (...) 这类事先能知道要插多少行的 simple insert,InnoDB 会在互斥量保护下,一次申请所需数量的自增值,申请完成后很快释放,不必整条语句都持有表级 AUTO-INC
  • 对于 INSERT ... SELECTLOAD DATA 这类事先不知道会插多少行的 bulk insert,仍然会使用表级 AUTO-INC 锁,并在语句执行过程中逐步分配自增值

因此,这一模式兼顾了两件事:

  • simple insert 的并发性能明显更好
  • 语句级别的自增值仍然更容易保持连续,适合 statement-based replication

2:interleaved

这是更激进的并发模式,也是 MySQL 8.x 默认更偏好的方向。它不再对 INSERT-like 语句使用表级 AUTO-INC 锁,而是统一走更轻量的分配控制。

  • 优点是并发度最高,多个插入语句可以同时推进
  • 代价是不同语句之间拿号会交错,同一条语句拿到的自增值也可能出现间洞
  • 它保证的是“唯一且总体递增”,不是“绝对连续”

这里要特别纠正一句容易说过头的话:它影响的不是所有主从同步,而是 statement-based replication 下的确定性

  • 如果使用 statement-based replication,innodb_autoinc_lock_mode=2 可能让主库和从库在重放 SQL 时分配出不同的自增值
  • 如果使用 row-based replication 或 mixed format,这个问题通常就是可控的,这也是为什么新版本默认更敢采用更高并发的模式

10.5 工程上真正该记住什么

这部分最后可以压缩成几条判断:

  1. 自增锁的核心不是“锁行”,而是“保护自增计数器分配”。
  2. 自增 ID 不重复,不等于事务已经安全持久化到磁盘。
  3. 所谓“新版本用互斥量、老策略用表级锁”的说法,核心要落实到 innodb_autoinc_lock_mode 以及语句类型上。
  4. 已知插入量的 simple insert 更容易做批量申请;未知插入量的 bulk insert 更容易退回到保守策略。
  5. 自增值不连续并不一定是异常,回滚、失败、批量预留未用完,都可能造成间洞。
  6. 真要排查“大并发插入为什么慢”,通常要同时看自增锁模式、语句形态、innodb_flush_log_at_trx_commitsync_binlog 和复制格式。

11. 日志系统要分清 binlog、redo log、undo log

11.1 binlog

如果先用一句最通俗的话来记:

  • binlog 更像 MySQL Server 记下的“对外变更流水”

它是 Server 层的二进制日志,记录的是数据变更事件,主要用于:

  • 主从复制
  • 按时间点恢复(PITR)
  • 审计和回放分析

它不是“只记录 SQL 原文”。在不同 binlog_format 下,binlog 可能记录:

  • SQL 语句本身
  • 行级变更结果
  • 或二者混合

所以理解 binlog 的重点不是“它长什么样”,而是:

  • 它站在 MySQL Server 的视角记录“这次事务对外做了什么变更”
  • 后续主从复制、时间点恢复,主要都依赖它

11.2 redo log

如果也用一句最通俗的话来记:

  • redo log 更像 InnoDB 记下的“内部施工日志”

redo log 是 InnoDB 层的预写日志(WAL),核心作用是保证崩溃恢复能力。

为什么需要它?

因为数据库真正的数据页,不会在每次事务提交时都立刻刷回磁盘。否则每次修改一行都要直接改磁盘上的页,随机 I/O 太重,性能会很差。

所以 InnoDB 的思路是:

  1. 先在内存里的 Buffer Pool 修改数据页
  2. 同时把这次修改对应的 redo 信息记下来
  3. 事务提交时,优先保证 redo log 按策略落到安全位置
  4. 真实数据页之后再慢慢刷盘

这就是 WAL 的核心思想:先记日志,再慢慢写数据页

理解 redo log 时要抓住两件事:

  • 事务提交时,首先要保证对应 redo 已经按策略写入或刷新到安全位置
  • 真正的数据页可以稍后再刷回表空间文件

因此,更准确的说法是:

  • redo log 负责“先把这次物理修改保住”
  • 不是“等数据页真的刷盘后事务才算提交”

如果数据库突然宕机,重启后 InnoDB 就可以根据 redo log,把那些“已经提交但数据页还没来得及刷盘”的修改重新补上。

关于 innodb_flush_log_at_trx_commit,常见记忆如下:

  • 1:每次提交都把 redo 刷到磁盘,持久性最强,也是默认配置
  • 2:每次提交写到操作系统缓存,通常每秒再刷盘一次
  • 0:提交时不主动刷,通常每秒统一处理一次,性能更高但风险也更大

11.3 undo log

undo log 一方面为事务回滚服务,另一方面为 MVCC 提供历史版本链。

可以把它理解成“并发可见性”和“失败可撤销”共用的一套基础设施。

11.4 binlog 和 redo log 为什么要一起看

很多人第一次学到这里时会困惑:

  • 既然有 redo log,为什么还需要 binlog?
  • 既然有 binlog,为什么还需要 redo log?

最根本的原因是:它们解决的不是同一个问题。

可以先用一个非常粗糙但好记的类比:

  • redo log 像饭店后厨自己的“备餐记录”,重点是后厨断电后还能接着做
  • binlog 像饭店给总店留的“出单流水”,重点是别的门店和审计系统也能知道今天卖了什么

也可以直接记成下面这张对比表:

维度 redo log binlog
所属层次 InnoDB 存储引擎层 MySQL Server 层
记录内容 更偏底层的页修改信息 更偏逻辑层的变更事件
主要作用 崩溃恢复、保证持久性 主从复制、时间点恢复
写入时机 事务执行过程中持续产生,提交时按策略刷出 事务提交时写入
是否循环覆盖 通常循环写 通常顺序追加写
能否替代对方 不能 不能

把这张表翻成白话,就是:

  • redo log 主要回答“这次提交,机器突然掉电后,InnoDB 能不能把数据补回来”
  • binlog 主要回答“这次提交,别的副本能不能知道、以后能不能按时间点把它重放出来”

再举一个更直观的例子。

假设你执行了:

1
UPDATE account SET balance = balance - 100 WHERE id = 1;

这次事务提交时,大致可以这样理解:

  • InnoDB 会生成 redo log,表示某些数据页被怎样修改过
  • MySQL Server 会生成 binlog,表示这次事务产生了什么变更事件

二者看的是同一件事,但视角不同:

  • redo log 更像“页 1234 的某个位置从什么变成什么”
  • binlog 更像“account 表里哪一行发生了怎样的业务变更”

所以:

  • 只靠 redo log,主从复制很难直接做,因为它太偏 InnoDB 内部实现
  • 只靠 binlog,崩溃恢复又不够高效,因为它不是给 InnoDB 做页级恢复设计的

业务提交事务时,MySQL 既要保证 InnoDB 自己能在崩溃后恢复,也要保证复制和时间点恢复所依赖的 binlog 不丢语义。

这就是为什么工程上经常把 redo logbinlog 放在一起讲:两者分别属于不同层,但提交时必须保持一致。

可以先抓住高层逻辑:

  • redo log 负责把存储引擎内的数据变更“保住”
  • binlog 负责把 Server 层的变更事件“记下来”
  • 两阶段提交机制负责尽量避免“只有一边成功、另一边缺失”的不一致

如果你只想记一句话,可以记成:

  • redo log 是给 自己崩溃恢复 用的
  • binlog 是给 复制和恢复链路 用的

如果只理解单个日志,很容易解释不清主从复制、崩溃恢复和事务提交之间为什么能衔接起来。

12. 主从复制先看链路,再看一致性等级

12.1 主从同步的数据流向

经典异步复制链路可以概括为:

主库提交事务 -> 写入 binlog -> 复制线程把 binlog 发送给从库 -> 从库写入 relay log -> 从库回放事件 -> 数据追平

12.2 三种常见 binlog 格式

statement

记录 SQL 语句本身。

优点是日志更小;缺点是某些非确定性函数、依赖上下文的语句,更容易带来主从不一致风险。

row

记录行级变更。

优点是一致性最好;缺点是日志体积通常更大。

mixed

在 statement 和 row 之间折中,由 MySQL 视情况切换。

12.3 异步复制和半同步复制差在哪

异步复制

主库提交后即可向客户端返回成功,不等从库确认。

优点是延迟低;缺点是主库故障时,最近已提交事务可能还没传到任何从库。

异步复制.png

半同步复制

主库会等待至少一个从库确认“已经收到并记录了事务事件”,再返回成功。

这里最容易写错的一点是:半同步复制等待的是从库收到并写入日志的确认,不是等待从库已经执行完事务。

半同步复制.png

12.4 AFTER_SYNC 和 AFTER_COMMIT 的区别

MySQL 半同步复制还可以配置等待点:

  • AFTER_COMMIT:主库先提交到存储引擎,再等从库确认
  • AFTER_SYNC:主库先写并同步 binlog,等从库确认后,再提交到存储引擎

工程上更常提到的是 AFTER_SYNC,因为它能进一步缩小故障切换时的事务丢失窗口。它解决的是复制确认时机问题,不是事务隔离里的“幻读”问题。

12.5 再看一眼几种复制确认时机

如果只是记概念,异步复制和半同步复制已经够用了;但如果是为了复习和面试,最好再把“增强半同步”和“全同步”也放到同一张脑图里。

增强半同步复制 可以理解成在半同步复制基础上,进一步优化确认时机和事务丢失窗口控制的实现思路。它不是另一个完全独立的复制体系,而是对半同步复制行为的强化理解。

增强半同步复制.png

全同步复制 可以把它当成帮助理解一致性代价的对照概念:主库必须等所有副本都确认完成,才能向客户端返回成功。它能提供更强的一致性直觉,但延迟和可用性成本通常也最高,因此工程上并不是 MySQL 常规主流方案。

全同步复制.jpg

12.6 新从库怎么追主库

如果是传统方式初始化新从库,思路通常是:

  1. 在主库做一致性备份
  2. 把备份恢复到从库
  3. 让从库从备份时刻对应的 binlog 位点或 GTID 开始追

使用 mysqldump 时,下面这个组合比较常见:

1
mysqldump -u root -p --all-databases --single-transaction --master-data=2 > /tmp/master_data.sql

参数含义:

  • --single-transaction:对 InnoDB 表做一致性快照备份,尽量减少锁表影响
  • --master-data=2:把备份时的 binlog 位点写进导出文件注释中,便于后续建立复制

如果环境已经全面使用 GTID,后续通常会配合自动定位能力,而不是手工维护文件位点。

12.7 GTID 的工程价值是什么

GTID 可以把“这个事务是谁、执行到哪里了”变成全局唯一标识,而不是只靠文件名和位点去追进度。

它的直接收益主要有三类:

  • 主从切换时更容易定位复制进度
  • 新从库追主库时更容易自动补齐缺失事务
  • 故障恢复和拓扑调整时,人工判断成本更低

所以在现代部署里,GTID 更像是“复制管理能力”的基础设施,而不只是一个额外参数。

12.8 读写分离为什么容易让业务读到旧数据

很多业务把主库负责写、从库负责读,但一旦从库存在复制延迟,就可能出现“刚写完立刻去读,却读不到最新值”的现象。

这类问题本质上不是 SQL 写错,而是副本追平存在时间差。

常见应对方式包括:

  • 写后短时间内强制读主库
  • 关键链路不做读写分离
  • 通过半同步复制、监控复制延迟等方式缩小风险窗口
  • 业务端显式设计“读己之写”策略

因此,复制的核心问题从来不只是“能不能同步”,还包括“业务能不能接受这段延迟”。

13. UNIONUNION ALL 的真正区别

13.1 语义区别

  • UNION:合并结果并去重,等价于 UNION DISTINCT
  • UNION ALL:合并结果但不去重

13.2 性能差异

UNION 需要去重,通常比 UNION ALL 成本更高。

13.3 顺序问题

默认情况下,UNIONUNION ALL 的最终结果都不保证顺序。即便某次看起来像“按书写顺序返回”,那也不应该被当成稳定语义。

如果业务依赖顺序,必须在最外层写 ORDER BY

1
2
3
4
SELECT id, name FROM t1
UNION ALL
SELECT id, name FROM t2
ORDER BY id;

14. 最后把整篇内容压缩成几条判断

如果只想带走最重要的判断,可以记住下面几条:

  1. InnoDB 里真正决定查询和锁范围的,往往不是“表”,而是“索引页和索引范围”。
  2. 二级索引叶子节点存的是主键,不是行地址,所以回表是常态,不是特例。
  3. 联合索引能不能继续利用,关键看前面列是否还保持有序,而不是只背“范围后失效”。
  4. 普通 SELECT 默认是快照读,不是默认加共享锁。
  5. MVCC 解决的是读写并发可见性问题;锁解决的是当前读和写写冲突问题。
  6. redo log 负责崩溃恢复,binlog 负责复制和时间点恢复,undo log 负责回滚与历史版本。
  7. 半同步复制等待的是从库“收到并落日志”的确认,不是等待从库“执行完成”。
  8. UNION ALL 只是不过滤重复,不代表天然保序。