Files
cs-note/hhs/MySQL/07-高可用与分布式/29-Binary Log.md
T
2026-05-24 11:42:38 +08:00

292 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, Binary Log, binlog, WAL]
create time: 2026-05-16 00:00
---
# Binary Log(Binlog)
## 概述
Binlog 是 MySQL Server 层生成的**逻辑日志**,记录所有修改数据的 SQL 语句(或行变更)。它是主从复制、时间点恢复和增量备份的基础。
### 知识地图
```mermaid
flowchart LR
A["Binary Log"] --> B["主从复制<br/>从库通过 relay log 重放 binlog"]
A --> C["时间点恢复 PITR<br/>恢复到任意精确时刻"]
A --> D["增量备份<br/>基线备份 + binlog 增量"]
A --> E["审计分析<br/>追踪数据变更历史"]
A --> F["崩溃一致性<br/>与 Redo Log 2PC 协作"]
style A fill:#6A0DAD,color:#fff
```
> [!TIP] 读这一页之前建议先了解
> 对 Binlog 的理解建立在 InnoDB 存储引擎基础之上。**建议先看**: [[hhs/MySQL/04-存储引擎/19-InnoDB 深度解析]] → 本文档 → [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]]
## Binlog 与 Redo Log 的根本区别
```mermaid
graph LR
subgraph Redo_Log["Redo Log"]
R1["InnoDB Engine 专属"]
R2["物理日志<br/>记录页级修改"]
R3["循环写入<br/>固定大小"]
R4["保证崩溃恢复"]
end
subgraph Binlog["Binlog"]
B1["Server Layer 通用<br/>所有引擎都写"]
B2["逻辑日志<br/>记录 SQL 或行变更"]
B3["追加写入<br/>可无限增长"]
B4["保证主从一致 + PITR"]
end
style R1 fill:#C44569,color:#fff
style B1 fill:#00B6BC,color:#fff
```
### 关键对比
| 特性 | Redo Log | Binlog |
|------|----------|--------|
| **层级** | InnoDB 引擎层 | MySQL Server 层 |
| **范围** | 只有 InnoDB 使用 | 所有引擎都可使用 |
| **内容** | 物理:页偏移+字节修改 | 逻辑:SQL 或行变更 |
| **写入方式** | 循环覆盖 | 追加 append-only |
| **用途** | Crash Recovery | 主从复制、PITR、审计 |
| **2PC 协调** | Phase 1: Redo prepare | Phase 2: Binlog fsync → redo commit |
## Binlog Format 三种模式
### Statement 模式
每条修改数据的 SQL 作为一条 event 写入。主从复制时,从库直接执行这条 SQL:
```
# At 12345
#260516 10:30:00 server id 1 end_log_pos 12378 CRC32 0xabc123
UPDATE orders SET status = 'shipped' WHERE id = 100;
```
> [!QUESTION] Statement 模式简单直观,那为什么还要有其他模式?
>
> 关键问题:**NOW()、RAND()、UUID() 这些非确定性函数**。主库和从库的执行时间不同,结果就不同——这会导致主从数据不一致。例如:`UPDATE scores SET grade = NOW()` 在主库和执行时是 10:30:00,但从库可能在 10:30:01 执行,导致两个库的该记录值不同。
| 优点 | 缺点 |
|------|------|
| 体积小,binlog 少 | 某些函数(NOW(), RAND())结果在主从不一致 |
| 无需额外存储空间 | INSERT ... SELECT 产生大量 binlog |
| | 复杂触发器行为可能不同 |
### Row 模式
只记录行的实际变化(增删改前后的数据),不关心 SQL 是什么。从库拿到的是「数据快照」,直接重放即可:
```
### @1=100 (BIGINT)
### @2='pending' (VARCHAR(20))
### @3=50.00 (DECIMAL(10,2))
### @4='2026-05-16 10:30:00' (DATETIME)
### UPDATE `app_db`.`orders`
### WHERE @1=100
### @2='pending'
### @3=50.00
### @4='2026-05-16 10:30:00'
### SET
### @1=100
### @2='shipped'
### @3=50.00
### @4='2026-05-16 10:30:00'
```
> [!TIP] 理解 Row 模式的关键
> 上面每行 `@n=值 (类型)` 表示第 n 个列的值和 MySQL 内部类型。WHERE 子句定位原行,SET 子句给出新值。虽然比 Statement 模式冗长很多,但**完全不受函数非确定性影响**——这就是为什么生产环境强烈推荐 ROW。
| 优点 | 缺点 |
|------|------|
| 主从数据绝对一致 | 体积极大(尤其是大批量 UPDATE)|
| 支持 DDL 复制 | DELETE 全表扫描时极度膨胀 |
| 不依赖函数确定性 | |
| | BINLOG 格式无法直接阅读 |
### Mixed 模式
Statement 为主,遇到不确定情况切 Row:
| 触发条件 | 切换为 Row |
|---------|-----------|
| 表没有主键 | ✅ |
| 使用了 UUID() / RAND() | ✅ |
| INSERT DELAYED | ✅ |
| UPSERT (REPLACE/ON DUPLICATE KEY) | ✅ |
```ini
binlog_format = STATEMENT # 传统默认(已废弃)
binlog_format = ROW # 生产推荐 ✅
binlog_format = MIXED # 折中方案
```
> [!WARNING] 强烈建议使用 ROW 模式
> Oracle 官方在 5.7+ 已将默认值改为 ROW。Statement 模式下「主库正确但从库错误」是常见的线上事故原因。
### 总结:如何选择?
> [!QUESTION] 如果生产环境必须三选一,你会选哪个?
>
> 答案:**ROW**。理由很简单——数据一致性永远排在体积和可读性之前。虽然 ROW 模式的 binlog 体积更大,但它消除了所有不确定性,而节省空间带来的好处远不及一次主从不一致造成的损失。
### Row vs Statement 对特殊场景的影响
除了非确定性函数(`NOW()`、`RAND()`)之外,还有两类场景在 STATEMENT 模式下容易引发主从不一致:
| 场景 | 问题原因 |
|------|---------|
| **字符集 / Collation 不一致** | 主库和从库 collation 不同时,STATEMENT 模式下的字符串比较可能得出不同结果;ROW 模式传输原始字节,不受影响 |
| **自增 ID(auto_increment)** | 主从并发写入时,STATEMENT 模式下从库的自增值可能与主库不同步,导致重复或跳跃 |
| **INSERT_DELAYED** | 5.6+ 已移除,执行时机不确定 |
**结论**:生产环境一律使用 `binlog_format = ROW`,避免上述所有隐患。
## Binlog 文件结构
```
/var/lib/mysql/
├── mysql-bin.000001 ← 二进制日志文件
├── mysql-bin.000002 ← 自动增长
├── mysql-bin.000003
├── mysql-bin.index ← 索引文件,列出所有 binlog
└── binary.log ← 备用名称(某些配置)
```
### Binlog 文件组织
```mermaid
graph TD
A["mysql-bin.index"] -->|指向| B["mysql-bin.000001"]
A -->|指向| C["mysql-bin.000002"]
A -->|指向| D["mysql-bin.nnn"]
B --> E["Format Description Event"]
B --> F["Query Event"]
B --> G["Rows / Xid Events"]
C --> H["Format Description Event"]
style A fill:#FF8C00,color:#fff
```
> [!QUESTION] 为什么需要 index 文件?
> Binlog 是 append-only 的,新的事务不断写入。index 文件记录了所有有效的 binlog 文件列表,这样 MySQL 启动时可以快速定位到最新的 binlog 位置,而不用扫描整个磁盘。
### 四种核心 Event
| Event 类型 | 作用 | 示例 |
|-----------|------|------|
| **Query Event** | 执行 DDL 或非事务型 DML | CREATE TABLE, ALTER TABLE |
| **Rows Event** | 记录行变更(ROW 模式)| INSERT_ROWS, UPDATE_ROWS, DELETE_ROWS |
| **Xid Event** | 事务提交标记 | COMMIT #1001 |
| **Format Description Event** | 描述 binlog 版本和格式 | 每个文件第一条 |
## Binlog 核心参数
```ini
[mysqld]
server_id = 1 # 必须唯一,用于主从识别
log_bin = /var/log/mysql/mysql-bin # 开启并指定路径
binlog_format = ROW # 推荐 ROW
binlog_row_image = FULL # 完整行(可选值:FULL / MINIMAL / NOBLOB)
binlog_expire_logs_seconds = 604800 # 7 天自动清理
max_binlog_size = 100M # 单文件最大大小
sync_binlog = 1 # 每次 commit 都刷盘(安全)
gtid_mode = ON # GTID 模式
enforce_gtid_consistency = ON # 强制 GTID 兼容的事务
binlog_checksum = CRC32 # 完整性校验
```
### binlog_row_image 参数详解
> [!TIP] 为什么默认是 FULL?
> `FULL` 记录修改前后的所有列值,最简单也最安全。当某些列是 BLOB 类型且非常大时,可以改用 `MINIMAL`(只记录被修改的列 + 主键)来节省空间和性能。
| 值 | 行为 | 适用场景 |
|----|------|---------|
| **FULL** | 记录所有列的值 | 通用场景,默认值 ✅ |
| **MINIMAL** | 仅记录被修改的列 + 必要索引列 | 大表 UPDATE,减少 binlog 体积 |
| **NOBLOB** | 与 FULL 相同,但排除 BLOB/TEXT 列 | 包含大文本字段的表 |
### sync_binlog 与安全性
| 值 | 行为 | 性能损失 | 丢数据风险 |
|----|------|---------|-----------|
| **0** | OS 决定何时刷盘 | 最低 | 断电丢失整个缓冲区 |
| **1** | 每次 commit 都刷盘 | 最高 | 零 ❌ |
| **N** | 每 N 次 commit 刷一次 | 中等 | 最多丢 N 个事务 |
> [!TIP] sync_binlog 与 innodb_flush_log_at_trx_commit
> - 两者都设为 1 = 最强的 ACID 保证
> - 两者配合保证了即使服务器宕机也不会丢失任何已提交事务
> - 对 SSD 磁盘,sync_binlog=1 的性能损耗约 5%~10%,完全可接受
### gtid_mode 的作用
> [!TIP] 为什么推荐使用 GTID?
> 传统基于 Position 的主从复制在故障转移时需要手动计算 position,容易出错。GTID 为每个事务分配全局唯一 ID,复制时只需要知道"哪些事务已经执行过"即可,大大简化了主从切换和恢复流程。
### binlog 清理策略
> [!WARNING] 千万不要直接删除 binlog 文件!
>
> 误删会导致 MySQL 无法识别索引与磁盘文件的对应关系,引发启动失败或数据不一致。
| 方式 | 命令 | 说明 |
|------|------|------|
| **按时间自动清理** | `binlog_expire_logs_seconds = 604800` | 7 天过期自动回收 ✅推荐 |
| **手动清理** | `PURGE BINARY LOGS BEFORE '2026-05-09 00:00:00';` | 保留指定日期之前的 binlog |
| **清理到指定文件** | `PURGE BINARY LOGS TO 'mysql-bin.000003';` | 清除 000003 之前的所有 binlog |
| **清空所有(谨慎)** | `RESET MASTER;` | ⚠️ 仅用于测试环境 |
## 查看和解析 Binlog
### 服务端查询
```sql
-- 当前正在写的 binlog 文件(主从架构中查看主库状态)
SHOW MASTER STATUS\G
-- 列出所有 binlog 文件及大小
SHOW BINARY LOGS;
-- 查看当前 binlog 位置(5.7+ 推荐用法)
SHOW BINARY LOG STATUS\G
```
> [!TIP] `SHOW MASTER STATUS` vs `SHOW BINARY LOG STATUS`
> MySQL 8.0.23+ 更推荐使用 `BINARY LOG STATUS`,因为从库也可以执行这个命令(MASTER 一词在复制语境下对从库不语义准确)。两者功能相同。
### 命令行解析
```bash
# 命令行解析 binlog(-v 展开列名,DECODE-ROWS 以可读格式展示行变更)
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000001
# 按时间范围解析(常用于精确时间点恢复)
mysqlbinlog --start-datetime="2026-05-16 00:00:00" \
--stop-datetime="2026-05-16 12:00:00" \
mysql-bin.000001 > decoded.sql
# 按 position 位置解析(精确到事务边界)
mysqlbinlog --start-position=1234 --stop-position=5678 mysql-bin.000001
```
> [!QUESTION] 如何定位一个错误操作的具体 position?
>
> 1. 用 `mysqlbinlog --start-datetime="..." --stop-datetime="..." file | grep "DELETE FROM"` 找到可疑 SQL
> 2. 查看 SQL 上方的 `# at XXXXX` 确定起始 position
> 3. 再结合 `--stop-position` 可以只回滚特定事务区间
## 关联笔记
- [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] — Binlog 是主从复制的基石
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Binlog 与 Redo Log 的两阶段提交协议
- [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — 基于 binlog 的 PITR 时间点恢复