数据库基础体系 · 第 85/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

MySQL Redo、Undo 与 Binlog:提交链路、崩溃恢复和一致性

在 InnoDB 事务中,Redo、Undo 和 Binlog 分别解决不同问题:

  • Redo log:记录已经发生的页修改,主要用于崩溃后把已提交事务的修改重新做出来。
  • Undo log:记录如何撤销修改,以及如何为一致性读提供旧版本。
  • Binlog:记录服务器层面的事务变更,用于复制、增量恢复和审计式变更传播。

它们都可能包含“某行被更新”的信息,但记录对象、写入位置、生命周期和恢复用途不同。把三者简单理解成“都是日志”会导致很多错误判断,例如:

  • 有 Binlog 就不需要 Redo;
  • 有 Undo 就能完成崩溃恢复;
  • Binlog 已经存在就代表事务一定对外可见;
  • COMMIT 返回成功就意味着所有日志都已安全落盘;
  • 从库已经收到 Binlog 就代表从库已经提交事务。

要理解这些问题,需要先区分 MySQL Server、InnoDB、文件系统和复制线程分别承担的职责。


一、先建立四个层次:谁在处理事务

一次普通的 InnoDB 事务至少涉及以下层次:

客户端
  │ SQL、COMMIT
  ▼
MySQL Server 层
  │ 解析、优化、事务协调、Binlog
  ▼
InnoDB 存储引擎
  │ 数据页、索引、Undo、Redo、事务状态
  ▼
操作系统与存储设备
  │ write、fsync、设备缓存
  ▼
磁盘介质

另外,复制链路在提交之后或提交过程中读取 Binlog:

主库事务
  ├─ InnoDB 数据页、Redo、Undo
  └─ Binlog
          │
          ▼
      从库接收
          │
          ▼
      Relay Log
          │
          ▼
      SQL/applier 线程执行并提交

这里有三个容易混淆的“成功”:

  1. SQL 执行成功:语句没有报错,修改可能仍在事务中。
  2. 事务提交成功:Server 和存储引擎完成提交协议,客户端收到成功。
  3. 数据持久化成功:在目标故障模型下,崩溃后仍可恢复出提交结果。

第 3 点依赖日志刷盘参数、存储设备行为、故障类型以及是否启用了复制。不能只根据客户端收到 Query OK 就推断所有副本和所有日志都已经持久化。


二、Redo Log:让 InnoDB 能够重做已提交修改

2.1 Redo 记录什么

InnoDB 的数据和索引以页为单位存放在表空间中。事务修改一行时,通常并不会立即把每个被修改的数据页同步写入磁盘。原因是随机写入数据页代价高,而且同一个页可能被多个事务继续修改。

因此 InnoDB 先把页修改对应的日志写入 Redo Log。Redo 的核心信息是:

某个数据页在某个位置应当进行什么修改。

从抽象角度,可以把一次页修改写成:

Redo record = (page_id, page_offset, change)

实际格式由 InnoDB 内部实现决定,并不是一个供应用直接解析的通用 SQL 日志。它通常更接近物理或物理-逻辑混合描述,而不是:

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

这也是 Redo 能高效用于崩溃恢复的原因:恢复时不需要重新执行 SQL 优化、锁竞争和业务条件判断,而是根据日志修复数据页。

2.2 WAL:先写日志,再写数据页

InnoDB 遵循 Write-Ahead Logging,简称 WAL。关键约束是:

Redo 持久化对应脏数据页持久化\text{Redo 持久化} \prec \text{对应脏数据页持久化}

其中 A ≺ B 表示 A 必须先于 B 成为持久状态。

例如,事务修改了数据页 P:

内存中的 P:已修改
Redo:       已生成但尚未刷盘
磁盘中的 P:旧内容

这是允许的,因为崩溃后可以用 Redo 重做 P。

但下面这种状态不能安全存在:

Redo:       尚未持久化
磁盘中的 P:已经是新内容

如果此时机器断电,磁盘保留了新数据页,但没有对应 Redo。InnoDB 无法依赖 WAL 约束完成可靠恢复。

注意,WAL 并不要求每次事务提交都把所有数据页写入磁盘。它只要求在数据页可能落盘之前,覆盖这些修改的 Redo 已经落盘。

2.3 Redo 的 LSN

Redo 使用 LSN(Log Sequence Number)表示日志序列位置。理解以下几个概念有助于阅读状态:

  • 生成位置:事务产生 Redo 的位置;
  • 已写入日志文件的位置:Redo 已经从内存写入日志文件;
  • 已刷盘位置:Redo 已经执行相应的 flush;
  • Checkpoint:某个位置之前的数据页原则上已经写回,使得恢复无需从更早位置重放。

可以用不严格对应内部字段的方式表示:

生成 LSN ───────────────►
已写入 LSN ───────────►
已刷盘 LSN ─────────►
Checkpoint LSN ─────►

恢复通常从合适的 checkpoint 开始扫描后续 Redo。LSN 的主要意义是组织日志和判断恢复范围,不是事务 ID,也不是 Binlog position。

2.4 Redo 的循环空间与检查点

Redo Log 通常是循环使用的。旧日志只有在其覆盖的脏页已经写回并且不再需要时,才能被覆盖。否则日志空间可能被耗尽,前台写入会受到阻塞或压力。

因此 Redo 空间的压力不仅表示“事务太多”,也可能表示:

  • 脏页刷新速度跟不上;
  • 有长事务阻止旧版本清理,间接加大系统压力;
  • 大事务产生大量修改;
  • 存储设备写入延迟高。

Redo 的主要职责是崩溃恢复和持久性,不是提供一致性读。读取旧版本是 Undo 和 MVCC 的工作。


三、Undo Log:回滚修改,也提供 MVCC 旧版本

3.1 Undo 记录什么

Undo Log 记录的是恢复到旧状态所需的信息。例如一个事务将:

balance: 1000 → 900

写入 Undo 后,Undo 中可能包含:

旧值 balance = 1000

对于插入、删除和更新,Undo 的含义不同:

  • 插入 Undo:事务插入的记录如果回滚,需要删除这次插入;
  • 更新 Undo:保存被更新字段的旧值,用于回滚和构造旧版本;
  • 删除相关 Undo:支持删除的回滚以及版本可见性判断。

Undo 并不等于一份完整的“事务前数据库副本”。它是围绕记录版本和事务操作组织的内部结构。

3.2 Undo 与 MVCC

InnoDB 的一致性读通常不直接读取锁住的当前版本,而是根据 Read View 判断某个版本是否可见;如果当前版本对该 Read View 不可见,就沿着 Undo 版本链向前寻找可见版本。

设某行版本链如下:

当前版本 V3:事务 T3 写入
      │ Undo
      ▼
版本 V2:事务 T2 写入
      │ Undo
      ▼
版本 V1:事务 T1 写入

对于一个 Read View,InnoDB 会根据创建时的活跃事务集合判断:

V3 不可见 → 回溯 Undo
V2 不可见 → 回溯 Undo
V1 可见   → 返回 V1

可以把一致性读的判断抽象为:

visible(v,R)\text{visible}(v, R)

其中:

  • vv 是某个记录版本;
  • RR 是 Read View;
  • 如果生成该版本的事务在 RR 创建时已经提交,且满足具体可见性规则,则版本可见。

这说明 Undo 的一个核心用途是:

让读事务在不阻塞写事务的情况下读取自己视图中的旧版本。

它与锁的关系也必须分清:

  • 一致性读主要依赖 MVCC 和 Undo;
  • 当前读,例如 SELECT ... FOR UPDATE,读取当前版本并参与锁定;
  • Undo 不能替代行锁、间隙锁或死锁检测。

这些机制共同决定事务隔离级别下的行为,但职责不同。

3.3 Undo 与事务回滚

事务执行过程中,如果语句失败或客户端执行 ROLLBACK,InnoDB 可以利用 Undo 撤销尚未提交的修改。

例如:

CREATE DATABASE IF NOT EXISTS demo;
USE demo;

CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance INT NOT NULL
) ENGINE = InnoDB;

INSERT INTO account VALUES (1, 1000);
COMMIT;

START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
ROLLBACK;

SELECT * FROM account;

预期结果:

+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
+----+---------+

ROLLBACK 的效果不是“从 Redo 中删除日志”。Redo 一旦生成,通常仍然是日志历史的一部分;InnoDB 使用 Undo 对数据修改进行逻辑撤销,并为相关页生成必要的 Redo,使这次回滚本身也能在崩溃后恢复。

3.4 Undo 为什么不能替代 Redo

假设事务 T 修改了数据页 P,但尚未提交:

磁盘中的 P:旧值
内存中的 P:新值
Undo:       保存旧值
Redo:       保存页修改

如果机器崩溃,InnoDB 需要处理两类事务:

  • 已提交事务:保留其修改,必要时利用 Redo 重做;
  • 未提交事务:撤销其修改,利用 Undo 回滚。

如果某些数据页已经带着未提交修改落盘,恢复时可能需要先通过 Redo 重建页状态,再通过 Undo 回滚未提交事务。因此 Undo 的存在不能消除 Redo 的需求。


四、Binlog:服务器层的逻辑变更日志

4.1 Binlog 记录什么

Binlog 是 MySQL Server 层的二进制日志,主要用于:

  • 主从复制;
  • 基于时间点的增量恢复;
  • 记录事务变更顺序;
  • 生成复制所需的 GTID 和事务事件。

Binlog 与 Redo 的关键差异是记录层次不同:

项目 Redo Log Undo Log Binlog
所属层 InnoDB InnoDB MySQL Server
主要内容 页修改 旧版本、回滚信息 逻辑变更事件
主要用途 崩溃恢复 回滚、MVCC 复制、增量恢复
是否理解 SQL 语义 主要不依赖 SQL 主要不依赖 SQL 受日志格式影响
是否用于构造一致性读
生命周期 循环日志,受 checkpoint 影响 受 purge 和长事务影响 按保留策略过期或删除

Binlog 不一定保存“完整的最终行镜像”。具体内容取决于 binlog_format

  • STATEMENT:记录语句;
  • ROW:记录行变更;
  • MIXED:由服务器在不同情况下选择语句或行格式。

生产环境中经常使用 ROW,因为它对很多非确定性语句和执行环境差异更稳健。但 ROW 也不意味着 Binlog 记录了整个数据库页;它记录的是行事件。

可以查看设置:

SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'sync_binlog';
SHOW VARIABLES LIKE 'binlog_row_image';

前提是当前账号有查看这些系统变量的权限,并且服务器允许相关操作。log_binON 才表示该实例启用了 Binlog。

4.2 Binlog 不记录 InnoDB 的全部内部状态

Binlog 通常不包含:

  • InnoDB 页的物理布局;
  • Buffer Pool 中哪些页是脏页;
  • Undo 版本链的内部结构;
  • Redo LSN;
  • 某个数据页是否已经落盘。

所以 Binlog 不能直接替代 InnoDB 的崩溃恢复。把一条 Binlog 重新执行一遍,也不是与直接执行 Redo 等价的过程:

Redo 恢复:修复页状态
Binlog 重放:重新执行逻辑变更

前者用于恢复本实例的存储状态,后者用于生成另一个实例上的逻辑结果。


五、一次提交到底发生了什么

5.1 从 SQL 到提交

考虑下面的事务:

START TRANSACTION;

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

UPDATE account
SET balance = balance + 100
WHERE id = 2;

COMMIT;

假设两个表都使用 InnoDB,且没有触发器、非事务表或外部系统副作用。粗略的数据流如下:

UPDATE
  │
  ├─ 加锁、修改 Buffer Pool 中的数据页
  ├─ 生成 Undo
  └─ 生成 Redo
       │
       ▼
COMMIT
  │
  ├─ 事务进入提交协调流程
  ├─ InnoDB 准备提交
  ├─ Server 写入 Binlog 事务事件
  ├─ 按配置 flush/fsync Binlog
  ├─ 完成引擎提交
  └─ 向客户端返回成功

这只是概念图。实际流程会受到 Binlog 是否启用、是否有多个存储引擎、组提交、参数设置和故障点影响。

5.2 为什么需要两阶段提交

当同时启用 InnoDB 和 Binlog 时,事务必须同时满足两个目标:

  1. InnoDB 不能提交,而 Binlog 没有对应事务;
  2. Binlog 不能显示事务已提交,而 InnoDB 最终丢失该事务。

否则会出现两类不一致。

情形 A:InnoDB 已提交,Binlog 丢失

主库 InnoDB:有转账结果
主库 Binlog:没有该转账
从库:        永远不会执行该转账

主从数据分叉,且基于 Binlog 的增量恢复也无法得到这次变更。

情形 B:Binlog 已写入,InnoDB 没有提交

主库 Binlog:有转账事件
主库 InnoDB:没有转账结果
从库:        执行后有转账结果

主库和从库也会分叉。

因此 Server 和 InnoDB 之间需要类似两阶段提交的协调过程:

准备阶段:
  InnoDB 将事务置为 prepared,并持久化恢复所需状态

提交记录阶段:
  Server 将事务写入 Binlog,并按配置刷盘

完成阶段:
  InnoDB 完成 commit,清理 prepared 状态

“类似两阶段提交”是一个重要表述。对使用多个存储引擎的事务,MySQL 还涉及跨引擎协调;对 InnoDB + Binlog,实践中通常讨论的是 Binlog 与 InnoDB 之间的两阶段提交和原子提交协议。

5.3 崩溃发生在各个阶段会怎样

设事务 T 修改了 InnoDB 表,并且 Binlog 已启用。

阶段 1:尚未 prepare 就崩溃

InnoDB:事务未进入 prepared
Binlog:没有完整提交事务

恢复后,未提交的内存修改不会作为已提交事务保留;需要的清理由 InnoDB 完成。

阶段 2:InnoDB 已 prepare,Binlog 尚未完成

InnoDB:存在 prepared 事务 T
Binlog:找不到 T 的完整提交记录

恢复过程不能简单地把 T 当成已提交事务。它需要根据协调日志和 Binlog 判断该事务是否完成了提交协议;如果没有足够证据证明提交,应回滚 prepared 事务。

阶段 3:Binlog 已完成,但 InnoDB 尚未完成最终 commit

InnoDB:prepared T
Binlog:有 T 的完整事务事件

恢复时可以根据 Binlog 中的事务记录判断 T 应完成提交,而不是回滚。这样就避免了“从库已执行、主库恢复后却没有”的不一致。

阶段 4:InnoDB 已提交,客户端尚未收到响应

InnoDB:已提交
Binlog:有对应事务
客户端:可能收到错误、超时或连接断开

这是典型的“结果未知”场景。客户端不能仅凭网络错误断言事务一定回滚,也不能断言一定提交。应使用业务幂等键、事务查询或补偿查询确认结果。

5.4 组提交不是改变语义

MySQL 会把多个并发事务组织成组提交,以减少锁竞争和 fsync 次数。组提交常被拆成若干概念阶段:

多个事务完成执行
    │
    ▼
Flush:把 Binlog 事件写入文件缓存
    │
    ▼
Sync:按配置执行 fsync
    │
    ▼
Commit:各事务完成引擎提交

不同版本和实现的内部阶段细节可能变化,但语义重点不变:

  • 事务仍然具有原子性;
  • 事务在 Binlog 中通常是完整边界;
  • 多个事务共享一次刷盘可以提高吞吐;
  • 共享一次刷盘并不表示事务之间可以互相看到未提交修改。

六、提交成功与持久化:innodb_flush_log_at_trx_commitsync_binlog

6.1 InnoDB Redo 的刷盘策略

innodb_flush_log_at_trx_commit 控制事务提交时 InnoDB 日志的写入和刷盘行为。常见语义可以概括为:

典型行为 主要风险
1 每次事务提交都写入并 flush Redo 延迟和 I/O 压力更高
2 每次提交写入操作系统缓存,周期性 flush 到设备 操作系统或进程级故障可能丢失最近日志
0 提交时不立即写入,周期性写入和 flush 进程或主机故障可能丢失更多最近事务

具体写入时机还受到后台线程和操作系统行为影响。1 也不能抵抗存储设备虚假确认、设备缓存未保护等问题。

6.2 Binlog 的刷盘策略

sync_binlog 控制 Binlog 文件的同步策略。常见含义是:

  • 1:每次事务提交都同步 Binlog;
  • 大于 1:累计若干事务后同步;
  • 0:由操作系统决定何时写入设备。

这影响的是 Binlog 在故障后的保留程度,而不是 InnoDB 数据页本身。

6.3 为什么通常同时考虑两个参数

如果目标是让“客户端已经成功提交的事务”在单实例崩溃后尽可能可靠地恢复,并且复制依赖 Binlog,常见的强持久性配置是:

innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

但这不是无条件的“绝对不丢数据”保证。仍然需要考虑:

  • 文件系统和存储设备是否真正执行了 flush;
  • 云盘或 RAID 控制器是否有掉电保护;
  • 主机是进程崩溃、内核崩溃还是突然断电;
  • Binlog 是否已经被复制到其他机器;
  • 事务提交后是否还需要远端副本确认。

相反,如果设置为更宽松的值,可能出现:

客户端已经收到 COMMIT 成功
机器随后断电
恢复后最近若干事务不存在

这里的“若干”不是固定数字,取决于刷盘周期、工作负载、设备和故障时间,不能用一个通用秒数替代验证。


七、一次具体事务的日志演化

考虑账户转账:

START TRANSACTION;

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

COMMIT;

设初始状态为:

id=1, balance=1000
id=2, balance=500

7.1 执行第一条 UPDATE 后

内存数据页:
  id=1, balance=900

Undo:
  id=1 的旧版本 balance=1000

Redo:
  记录数据页中 balance 从 1000 变为 900 的修改

Binlog:
  事务事件尚未形成完整提交边界

这时其他事务能否看到 900,取决于它是否是当前事务、读取方式、隔离级别和锁行为;普通一致性读不能简单理解为“直接读 Buffer Pool 当前值”。

7.2 执行第二条 UPDATE 后

内存数据页:
  id=1, balance=900
  id=2, balance=600

Undo:
  保存两个记录的旧版本

Redo:
  保存两个数据页修改

Binlog:
  可能已在事务缓存中积累事件,但事务尚未完成提交

如果此时执行 ROLLBACK

Undo 回滚:
  id=1 恢复为 1000
  id=2 恢复为 500

Redo:
  记录回滚导致的页修改

Binlog:
  不应产生一个代表成功转账提交的完整事务

如果执行 COMMIT

InnoDB prepare
  │
  ▼
Binlog 写入该事务的完整事件
  │
  ▼
Binlog 按 sync_binlog 策略同步
  │
  ▼
InnoDB 完成 commit

成功提交后的最终逻辑状态是:

id=1, balance=900
id=2, balance=600

事务的原子性要求这两个修改一起出现或一起不出现。任何一个只完成的状态都不是该事务的合法提交结果。


八、崩溃恢复:Redo、Undo 和 Binlog 如何配合

8.1 InnoDB 恢复的两个主要阶段

InnoDB 崩溃恢复可以用两个概念阶段理解。

阶段一:Redo 前滚

从 checkpoint 后的 Redo 开始,重新应用日志中的页修改,使数据页达到崩溃前应有的状态。

磁盘页:旧
  │
  ├─ 应用 Redo R1
  ├─ 应用 Redo R2
  └─ 应用 Redo R3
      ▼
恢复到崩溃前的页状态

Redo 应用通常要求操作具有幂等性或可判断是否已经应用,以便处理日志和数据页写入之间的重复情况。

阶段二:处理未完成事务

前滚后,系统可能发现一些事务:

  • 已完成提交;
  • 尚未提交;
  • 处于 prepared 状态;
  • 提交证据需要与 Binlog 协调判断。

对于不应保留的未提交修改,InnoDB 使用 Undo 执行回滚或延迟回滚相关工作。

简化表示:

Redo:恢复“崩溃时页应该是什么样”
Undo:撤销“最终不应提交的事务”
Binlog:帮助判断与复制相关的提交边界

8.2 恢复不是“把三种日志从头到尾顺序播放”

这是一种常见但错误的模型:

先播放 Redo
再播放 Undo
最后播放 Binlog

真实恢复由各组件的格式、LSN、事务状态和协调信息决定,不能把三种日志当成同一种事件流。

尤其是:

  • Redo 主要以页修改为单位;
  • Undo 以事务版本和回滚信息为单位;
  • Binlog 以逻辑事务事件为单位;
  • Binlog 重放通常用于另一个实例的复制或增量恢复,而不是本实例 InnoDB crash recovery 的简单最后一步。

8.3 恢复中的 prepared 事务

如果崩溃发生在两阶段提交中间,可能留下 prepared 事务。恢复需要判断:

prepared 事务 T 是否存在已持久化的 Binlog 提交证据?

若有,则完成提交;若没有,则回滚。这样才能同时满足:

InnoDB 已提交  ⇔  Binlog 有对应提交事务

这里的“有对应提交证据”不是让运维人员手工看几行文本,而是 MySQL 恢复流程根据内部日志和 Binlog 事务信息进行协调。


九、Binlog 复制中的一致性边界

9.1 主库提交、从库接收、从库执行是三个状态

对于事务 T,复制链路至少有以下状态:

T 在主库执行完成
  │
  ▼
T 写入主库 Binlog
  │
  ▼
从库 IO/接收线程收到 T
  │
  ▼
T 写入从库 Relay Log
  │
  ▼
从库 applier 执行 T
  │
  ▼
从库提交 T

因此下面三个问题的答案可能不同:

主库上的 T 是否已经提交?
从库是否已经收到 T?
从库是否已经提交 T?

可以通过 GTID、主从状态和应用延迟分别判断,不能只看网络连接正常。

9.2 半同步解决什么问题

半同步复制的目标是:主库提交时,至少等待某个从库对 Binlog 接收或确认达到相应语义,再向客户端返回成功。

但必须明确确认点:

  • 某些模式确认的是从库收到并写入 Relay Log;
  • 这不必然表示从库已经执行并提交;
  • 等待的是哪个从库、是否超时、超时后是否退化为异步,都由具体配置和版本语义决定。

因此半同步通常降低主库已提交而所有从库都没有 Binlog 的风险,但不等于“远端事务已经可读”。

如果业务要求切换后数据必须已经在新主库可见,应额外等待从库应用完成,并验证 GTID 或事务状态,而不是只依赖半同步 ACK。

9.3 Binlog 顺序与事务原子性

在同一个 Binlog 中,事务具有边界和顺序。对于 ROW 格式,一个事务可能包含多个行事件,但复制应用不应把它们视为可以任意提交的独立更新。

如果主库上一个转账事务修改两行:

BEGIN
row event: account 1
row event: account 2
COMMIT

从库的最终提交语义应当保持事务原子性,而不是先让其他读线程看到第一行已经变化、第二行尚未变化的中间提交状态。具体读可见性还取决于从库并行复制、隔离级别和查询时机,但事务边界不能被任意拆成多个独立提交。


十、Binlog 与增量恢复的完整链路

Binlog 也可用于基于时间点恢复,但它不能单独完成恢复。通常需要:

全量备份
  │
  ▼
恢复到某个时间点
  │
  ▼
从对应 Binlog position 或 GTID 开始重放
  │
  ▼
停止在目标时间或目标事务之前

示意命令:

mysqlbinlog \
  --start-position=123456 \
  --stop-datetime='2025-01-15 10:30:00' \
  mysql-bin.000123 \
| mysql -uroot -p

这条命令的含义是:

  • 读取指定 Binlog 文件;
  • 从指定 position 开始;
  • 生成截至目标时间的 SQL/事件流;
  • 交给另一个 MySQL 客户端执行。

使用前必须确认:

  1. 全量备份与 Binlog position/GTID 的对应关系;
  2. 目标实例的数据集是否与备份一致;
  3. 目标库是否会误执行不应恢复的事务;
  4. 是否包含 DDL、用户权限、非事务表或外部副作用;
  5. STATEMENT 格式语句是否受环境差异影响;
  6. 是否需要使用 --rewrite-db、过滤规则或 GTID 选项,并验证其语义。

不要把 mysqlbinlog 当成“直接打开 Binlog 文件并恢复 Redo”的工具。它是在解析服务器层的逻辑事件,并让另一个服务器执行这些事件。


十一、一个反例:为什么“Binlog 有了就算提交成功”不成立

假设发生以下故障:

1. 事务 T 的 Binlog 事件已经写入操作系统缓存
2. sync_binlog 不是 1,事件尚未真正同步到设备
3. InnoDB 也尚未将对应 Redo 安全刷盘
4. 机器突然断电

可能结果是:

Binlog 文件恢复后没有 T
InnoDB 恢复后也没有 T

这并不违反事务语义,因为客户端是否已经收到成功、相关日志是否已经持久化是不同问题。

再看另一个反例:

1. 主库 InnoDB 已经提交 T
2. 主库 Binlog 因参数或故障尚未安全持久化
3. 主库宕机
4. 从库没有收到 T
5. 发生故障切换

新主库可能缺少 T。若业务已经把旧主库的成功响应视为永久事实,就需要通过更强的远端确认、切换前一致性检查或业务补偿处理这个窗口。


十二、常见误解与正确边界

12.1 “Redo 是物理日志,所以能单独恢复业务”

不准确。

Redo 能够恢复 InnoDB 页和内部状态,使存储引擎回到一致的崩溃恢复状态;它不是 SQL 审计日志,也不负责复制到另一台 MySQL。业务层面需要 Binlog、备份或其他变更记录。

12.2 “Undo 只在 ROLLBACK 时使用”

不准确。

Undo 同时服务于:

  • 显式回滚;
  • 崩溃恢复中撤销未提交事务;
  • MVCC 一致性读;
  • 版本清理前的数据访问。

长事务会阻止某些旧版本被 purge,导致 Undo 膨胀,并增加一致性读遍历版本链的成本。

12.3 “提交后立刻把数据页写入磁盘,才算持久化”

不准确。

只要覆盖该修改的 Redo 已经按照配置安全持久化,数据页可以稍后写回。WAL 的设计就是用顺序日志替代每次提交时的数据页同步写入。

12.4 “Binlog position 就是事务 ID”

不准确。

Binlog position 是某个 Binlog 文件中的字节位置。一个事务包含多个事件,position 可能指向事务中的不同事件。GTID 更适合表达事务身份和复制进度,但 GTID 也不等于 InnoDB 的 LSN。

12.5 “从库 IO 线程追平就代表数据追平”

不准确。

IO 线程只负责接收并写入 Relay Log。还需要确认 applier 线程已经执行并提交相关事务。并行复制时,接收位置和已执行位置尤其可能不同。

12.6 “普通 SELECT 一定读最新提交值”

不准确。

InnoDB 普通一致性读通常读取 Read View 对其可见的版本;同一事务内不同语句的可见性还受隔离级别影响。需要读取当前版本并加锁时,应使用当前读,例如:

SELECT balance
FROM account
WHERE id = 1
FOR UPDATE;

但当前读会参与锁竞争,可能等待或死锁;它与 Undo 版本读取不是同一条路径。

12.7 “所有表都能参加 InnoDB 的原子事务”

不准确。

InnoDB 的事务保证不能自动覆盖非事务存储引擎,也不能自动回滚已经发送到外部系统的 HTTP 请求、消息或文件写入。若一个事务同时涉及 InnoDB 和非事务操作,必须单独分析故障后的补偿和幂等性。


十三、如何诊断日志与一致性问题

13.1 检查实例是否启用 Binlog

SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'sync_binlog';
SHOW BINARY LOGS;

重点确认:

  • log_bin 是否为 ON
  • Binlog 文件是否持续生成;
  • 保留策略是否可能在备份前删除所需日志;
  • 日志格式是否符合复制和恢复需求。

13.2 检查 InnoDB Redo 与事务状态

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW ENGINE INNODB STATUS\G

SHOW ENGINE INNODB STATUS 可以辅助观察事务、锁、日志和后台活动,但输出是诊断快照,不应被当作永久监控接口。

还应结合:

  • 长事务;
  • 活跃事务列表;
  • 锁等待和死锁日志;
  • 磁盘 I/O 延迟;
  • Redo 使用压力;
  • checkpoint 推进情况。

13.3 检查复制接收与应用进度

不同版本和复制配置下,命令输出字段会有所不同,可先执行:

SHOW REPLICA STATUS\G

重点关注:

  • 接收线程是否运行;
  • 应用线程是否运行;
  • Relay_Log_Pos 与已执行位置;
  • Seconds_Behind_Source 的局限;
  • Last_IO_Error 和 Last_SQL_Error;
  • GTID 已接收与已执行集合。

Seconds_Behind_Source 不是严格的数据一致性证明。例如主库没有持续产生事务、从库系统时间不一致或并行应用存在空洞时,这个值都可能误导判断。切换验证应优先使用 GTID 和目标事务的实际可见性。

13.4 诊断“客户端超时但事务是否成功”

遇到以下错误:

ERROR 2013: Lost connection to MySQL server during query

或者客户端在 COMMIT 后连接断开,不能直接执行重试写入。正确的问题是:

原事务是否已经提交?

可采用:

  1. 业务请求携带唯一幂等键;
  2. 将幂等键与事务结果写入同一 InnoDB 事务;
  3. 连接恢复后按幂等键查询;
  4. 只有确认未提交时才重新执行。

例如:

CREATE TABLE payment_request (
    request_id VARCHAR(64) PRIMARY KEY,
    payment_id BIGINT NOT NULL,
    status ENUM('SUCCESS') NOT NULL
) ENGINE = InnoDB;

业务可以把 request_id 作为唯一约束。即使客户端在提交响应阶段断线,重试时也不会无条件产生第二笔业务结果。数据库事务只能保证数据库内的原子性,幂等键负责处理“响应丢失但提交结果未知”的网络边界。


十四、参数和部署取舍

14.1 强持久性与性能

常见取舍如下:

innodb_flush_log_at_trx_commit=1
sync_binlog=1

优点:

  • 单实例崩溃后,最近成功提交事务的恢复语义更强;
  • Binlog 与 InnoDB 的持久性窗口更小;
  • 复制和增量恢复的基础更可靠。

代价:

  • 提交路径更依赖日志 flush;
  • 存储延迟会直接影响事务提交延迟;
  • 高并发下需要依赖组提交、快速存储和合理批量事务。

放宽参数可以降低刷盘频率,但代价是接受故障窗口内的事务丢失风险。这个选择必须与业务的 RPO(可接受数据丢失范围)一致,不能只根据吞吐量作决定。

14.2 主库和从库的参数不应机械相同

主库通常更关注:

  • 客户端已确认提交的事务是否可恢复;
  • Binlog 是否可靠;
  • 故障切换后是否有足够复制进度。

从库还要考虑:

  • Relay Log 和本地 InnoDB 提交;
  • 是否允许从库重建;
  • 从库是否承担读流量;
  • 是否把从库作为切换目标。

如果从库用于提供强一致读或作为故障切换候选,仅确保“主库 Binlog 已发送”通常不够,还要验证从库已执行到目标 GTID。

14.3 长事务是三类问题的交汇点

长事务可能同时导致:

  1. Undo 版本无法及时 purge;
  2. 一致性读需要遍历更长版本链;
  3. 持有锁时间更长,增加锁等待和死锁概率;
  4. 事务提交时需要处理更大的日志和事务上下文;
  5. 复制应用和切换确认更复杂。

因此“日志空间不足”不一定只是 Redo 配置问题,也可能是事务生命周期管理问题。


十五、把整个链路压缩成一组不变量

理解 Redo、Undo 和 Binlog,最终要抓住以下不变量。

15.1 原子性不变量

对一个事务 T:

Committed(T)所有 InnoDB 修改一起可见\text{Committed}(T) \Rightarrow \text{所有 InnoDB 修改一起可见}

反之,未提交事务的修改不能作为已提交结果对普通事务永久可见。

15.2 WAL 不变量

若数据页修改 PP 可能先于提交写入磁盘,则:

PersistedRedo(P)\text{PersistedRedo}(P)

必须成立。也就是说,数据页可以晚写,但不能让它领先于覆盖它的 Redo。

15.3 Binlog 与引擎提交的一致性不变量

在启用 Binlog 的事务协调范围内,应尽量维护:

InnoDBCommit(T)BinlogCommitRecord(T)\text{InnoDBCommit}(T) \Longleftrightarrow \text{BinlogCommitRecord}(T)

两阶段提交就是为了让崩溃恢复能够处理这两个状态暂时分离的中间窗口。

15.4 MVCC 不变量

对 Read View RR,查询返回的是满足可见性规则的版本:

Result(R)=最新的 v使visible(v,R)=true\text{Result}(R) = \text{最新的 } v \quad\text{使}\quad \text{visible}(v,R)=\text{true}

当当前版本不可见时,沿 Undo 版本链回溯,而不是简单读取磁盘上的旧页。

15.5 复制一致性不变量

复制中至少要区分:

Received(T)Applied(T)Readable(T)\text{Received}(T) \neq \text{Applied}(T) \neq \text{Readable}(T)

从库收到事务、从库提交事务、客户端能够读到该事务,是三个不同事件。


十六、最终的故障分析方法

遇到“数据丢失、主从不一致、恢复后结果异常”时,可以按以下因果顺序排查,而不是先猜某一种日志坏了:

1. 客户端是否收到提交成功?
2. 事务是否真的进入 InnoDB commit?
3. Redo 是否按配置持久化?
4. Binlog 是否生成完整事务?
5. Binlog 是否同步到设备?
6. 从库是否收到该 GTID?
7. 从库是否执行并提交该 GTID?
8. 查询使用的是一致性读还是当前读?
9. 是否存在长事务、锁等待或未完成 prepared 事务?
10. 故障属于进程崩溃、主机断电、存储故障还是人为切换?

对应关系可以概括为:

Redo:崩溃后如何修复 InnoDB 页
Undo:如何回滚,以及如何找到可见旧版本
Binlog:如何把逻辑事务传播到其他实例,或用于增量恢复
两阶段提交:如何让引擎提交与 Binlog 提交边界保持一致
复制确认:如何判断远端只收到、已应用,还是已经可读

三者不是互相替代的三份副本,而是位于不同层次、服务于不同一致性问题的协作机制。只有把“事务执行”“事务提交”“日志持久化”“崩溃恢复”“复制应用”和“客户端可见性”分别建模,才能正确解释 MySQL 在正常路径和故障路径下的行为。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。