292 lines
11 KiB
Markdown
292 lines
11 KiB
Markdown
---
|
||
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 时间点恢复
|