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

11 KiB
Raw Blame History

tags, create time
tags create time
MySQL
Binary Log
binlog
WAL
2026-05-16 00:00

Binary Log(Binlog)

概述

Binlog 是 MySQL Server 层生成的逻辑日志,记录所有修改数据的 SQL 语句(或行变更)。它是主从复制、时间点恢复和增量备份的基础。

知识地图

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 的根本区别

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) ✅
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 文件组织

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 核心参数

[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

服务端查询

-- 当前正在写的 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 一词在复制语境下对从库不语义准确)。两者功能相同。

命令行解析

# 命令行解析 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 可以只回滚特定事务区间

关联笔记