日志与崩溃恢复
三种日志的分工
| 日志 | 层级 | 内容 | 用途 |
|---|---|---|---|
| redo log | InnoDB 引擎层 | 物理日志:某个数据页上做了什么改动 | 崩溃恢复(持久性) |
| undo log | InnoDB 引擎层 | 逻辑日志:反向操作 | 事务回滚(原子性)+ MVCC |
| binlog | MySQL 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 缓存,每秒 fsync | MySQL 进程崩溃不丢,机器掉电丢 1 秒 |
这是性能与安全的经典取舍题。答「生产必须 1」是对的,但要补充:如果是可重建的缓存类数据,设 2 能显著提升写入吞吐。
binlog 与 redo log 的四个区别
- 归属:binlog 是 Server 层,所有引擎都有;redo log 是 InnoDB 独有
- 内容:binlog 是逻辑日志(「把 id=2 这行的 c 加 1」);redo log 是物理日志(「在第 N 页第 M 偏移处改成 X」)
- 写法:binlog 追加写,写满切新文件,历史文件都保留;redo log 循环写,会覆盖
- 用途: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 一旦写入就可能已被从库消费,回滚会导致主少从多。
主从复制
流程:
- 主库事务提交,写 binlog
- 主库的 dump 线程把 binlog 发给从库
- 从库 IO 线程接收,写入本地 relay log(中继日志)
- 从库 SQL 线程读 relay log 并重放
三种模式:
| 模式 | 主库何时返回 | 风险 |
|---|---|---|
| 异步复制(默认) | 写完 binlog 即返回 | 主库崩溃可能丢数据 |
| 全同步复制 | 所有从库都重放完 | 性能差,可用性受从库拖累 |
| 半同步复制 | 至少一个从库确认收到 binlog | 折中,生产常用 |
主从延迟的原因与解法:
- 从库配置低 / 承担大量读查询 → 提升配置、分摊读流量
- 大事务(一次删百万行)→ 拆成小批次
- 从库早期是单线程重放 → MySQL 5.7+ 开启并行复制(
slave_parallel_type = LOGICAL_CLOCK),按组提交并行 - 大表 DDL → 用在线改表工具
延迟导致的「写后读不到」:刚写入主库就查从库,可能读到旧数据。三种解法:强制走主库读(最简单可靠)、判断 seconds_behind_master 为 0 再读从库、用 GTID 等位点(WAIT_FOR_EXECUTED_GTID_SET)。