跳到主要内容

日志与崩溃恢复

三种日志的分工​

日志层级内容用途
redo logInnoDB 引擎层物理日志:某个数据页上做了什么改动崩溃恢复(持久性)
undo logInnoDB 引擎层逻辑日志:反向操作事务回滚(原子性)+ MVCC
binlogMySQL Server 层逻辑日志:原始 SQL 或行变更主从复制、数据恢复

redo log 与 WAL​

为什么需要 redo log? 事务提交时如果直接把数据页刷盘,是随机 IO,很慢;而且一个事务可能改多个分散的页。WAL(Write-Ahead Logging)的思路是:改动先顺序写 redo log,数据页留在内存(Buffer Pool)里当脏页,之后再择机批量刷盘。顺序写比随机写快两个数量级。

结构:固定大小的环形文件组,写满就回头覆盖。两个指针 —— write pos 当前写位置,checkpoint 可擦除位置。两者之间是待落盘的记录;追上了就必须停下来推进 checkpoint(刷脏页),此时写入会卡住。所以 redo log 不宜配得太小。

刷盘时机由 innodb_flush_log_at_trx_commit 控制:

值行为崩溃丢失
0每秒由后台线程写入并 fsync最多 1 秒数据
1(默认)每次提交都 fsync不丢(真正的 ACID)
2每次提交写入 OS 缓存,每秒 fsyncMySQL 进程崩溃不丢,机器掉电丢 1 秒
提示

这是性能与安全的经典取舍题。答「生产必须 1」是对的,但要补充:如果是可重建的缓存类数据,设 2 能显著提升写入吞吐。

binlog 与 redo log 的四个区别​

  1. 归属:binlog 是 Server 层,所有引擎都有;redo log 是 InnoDB 独有
  2. 内容:binlog 是逻辑日志(「把 id=2 这行的 c 加 1」);redo log 是物理日志(「在第 N 页第 M 偏移处改成 X」)
  3. 写法:binlog 追加写,写满切新文件,历史文件都保留;redo log 循环写,会覆盖
  4. 用途:binlog 做复制和时间点恢复;redo log 做崩溃恢复

binlog 三种格式:

  • STATEMENT —— 记原始 SQL。省空间,但 NOW()、UUID()、LIMIT 不带 ORDER BY 等不确定性语句会导致主从不一致
  • ROW —— 记每行变更前后的值。安全,但一条 UPDATE 影响 10 万行就写 10 万条记录
  • MIXED —— 由 MySQL 判断,不确定性语句用 ROW,其余用 STATEMENT

生产推荐 ROW,因为主从一致性优先,且数据恢复时能精确还原。

两阶段提交​

为什么要两阶段提交? redo log 和 binlog 是两个独立的文件,如果按顺序分别写,崩溃点落在中间就会不一致:

  • 先写 redo 后写 binlog,写完 redo 崩溃 → 恢复后主库有这条数据,但 binlog 缺失 → 从库少数据
  • 先写 binlog 后写 redo,写完 binlog 崩溃 → 主库恢复后没这条数据,但从库按 binlog 执行了 → 从库多数据

所以 InnoDB 用 2PC 把两者绑成一个原子操作:

1. 写 redo log,标记为 prepare 状态
2. 写 binlog,并 fsync
3. 写 redo log,标记为 commit 状态

崩溃恢复的判定规则:扫描 redo log,

  • 记录是 commit 状态 → 直接提交
  • 记录是 prepare 状态 → 去 binlog 里找对应的事务(靠 XID 关联):
    • binlog 完整存在 → 提交(因为 binlog 已经可能传给从库了)
    • binlog 不完整 → 回滚

这套规则保证了主从一致。面试追问「为什么 prepare 状态下有完整 binlog 就提交」—— 答:binlog 一旦写入就可能已被从库消费,回滚会导致主少从多。

主从复制​

流程:

  1. 主库事务提交,写 binlog
  2. 主库的 dump 线程把 binlog 发给从库
  3. 从库 IO 线程接收,写入本地 relay log(中继日志)
  4. 从库 SQL 线程读 relay log 并重放

三种模式:

模式主库何时返回风险
异步复制(默认)写完 binlog 即返回主库崩溃可能丢数据
全同步复制所有从库都重放完性能差,可用性受从库拖累
半同步复制至少一个从库确认收到 binlog折中,生产常用

主从延迟的原因与解法:

  • 从库配置低 / 承担大量读查询 → 提升配置、分摊读流量
  • 大事务(一次删百万行)→ 拆成小批次
  • 从库早期是单线程重放 → MySQL 5.7+ 开启并行复制(slave_parallel_type = LOGICAL_CLOCK),按组提交并行
  • 大表 DDL → 用在线改表工具

延迟导致的「写后读不到」:刚写入主库就查从库,可能读到旧数据。三种解法:强制走主库读(最简单可靠)、判断 seconds_behind_master 为 0 再读从库、用 GTID 等位点(WAIT_FOR_EXECUTED_GTID_SET)。