MySQL Binlog
大约 14 分钟约 4083 字
MySQL Binlog
简介
Binlog(二进制日志)是 MySQL Server 层的逻辑日志,记录所有对数据进行修改的操作(DDL 和 DML)。它是 MySQL 数据流转的枢纽——主从复制依赖它同步数据,CDC 工具通过它捕获变更,DBA 通过它做按时间点恢复。理解 Binlog 的格式、刷盘策略和消费机制,是搭建高可用架构和数据同步管道的前提。
Binlog 在 MySQL 架构中的位置
MySQL 事务提交流程:
客户端 → SQL 层 → InnoDB 引擎 → Redo Log(引擎层日志)
↓
Binlog(Server 层日志)
↓
主从复制 / CDC / PITR
两阶段提交(2PC)保证 Redo Log 和 Binlog 一致:
1. InnoDB 写 Redo Log(prepare 阶段)
2. SQL 层写 Binlog
3. InnoDB 提交事务(commit 阶段)
4. Binlog 刷盘
崩溃恢复时:
- Redo Log 中有 commit 标记 → 提交
- Redo Log 中有 prepare 但无 commit → 检查 Binlog:
- Binlog 中有对应事务 → 提交
- Binlog 中无对应事务 → 回滚三种 Binlog 格式
| 格式 | 记录内容 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| STATEMENT | SQL 语句 | 日志体积小 | 某些函数结果不一致 | 不推荐 |
| ROW | 行变更前后值 | 数据一致性强,安全 | 日志体积大 | 推荐 |
| MIXED | 自动选择 | 兼顾两者 | 行为不可预测 | 不推荐 |
STATEMENT 格式的问题
-- STATEMENT 格式下的问题函数
INSERT INTO logs (id, created_at) VALUES (1, NOW());
-- 主库执行:NOW() = '2024-01-15 10:00:00'
-- 从库延迟执行:NOW() = '2024-01-15 10:05:00'
-- 数据不一致!
INSERT INTO users (id, uuid) VALUES (1, UUID());
-- 每次执行 UUID() 结果不同,主从数据不一致
-- STATEMENT 格式适合的场景
-- 简单的 INSERT/UPDATE/DELETE,不涉及不确定函数
-- 如:UPDATE accounts SET balance = balance + 100 WHERE id = 1ROW 格式的优势与代价
-- ROW 格式记录行变更前后值
-- UPDATE accounts SET balance = 800 WHERE id = 1
-- Binlog 记录:table=accounts, before={id:1, balance:1000}, after={id:1, balance:800}
-- ROW 格式的体积问题
-- 批量 UPDATE 影响百万行,Binlog 记录每一行的变更
UPDATE orders SET status = 'archived' WHERE created_at < '2023-01-01';
-- 如果影响 100 万行,Binlog 会记录 100 万行的变更
-- 解决方案:分批执行,每批 1000-5000 行
-- ROW 格式支持 binlog_row_image 控制记录内容
SHOW VARIABLES LIKE 'binlog_row_image';
-- FULL(默认):记录变更前后的完整行
-- MINIMAL:只记录变更的列和必要列(主键、时间戳)
-- NOBLOB:不记录 BLOB/TEXT 列
-- 建议生产环境使用 MINIMAL 减少日志体积实战示例
示例 1:Binlog 配置与查看
-- 查看 Binlog 是否开启
SHOW VARIABLES LIKE 'log_bin';
-- MySQL 8.0 默认开启
-- 查看 Binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
-- 推荐设为 ROW
-- 查看 Binlog 行镜像模式
SHOW VARIABLES LIKE 'binlog_row_image';
-- 推荐设为 MINIMAL
-- 查看当前 Binlog 文件和位点
SHOW MASTER STATUS;
-- File: mysql-bin.000003
-- Position: 154
-- Binlog_Do_DB: (空,表示所有库)
-- Binlog_Ignore_DB: (空)
-- 查看所有 Binlog 文件列表
SHOW BINARY LOGS;
-- 列出所有 Binlog 文件及其大小
-- 查看 Binlog 事件内容
SHOW BINLOG EVENTS IN 'mysql-bin.000003' LIMIT 10;
-- 可以看到 Format_desc、Table_map、Write_rows 等事件
-- 使用 mysqlbinlog 工具解析(命令行)
-- mysqlbinlog --base64-output=decode-rows -v mysql-bin.000003
-- -v 显示伪 SQL(ROW 格式),-vv 显示更详细的信息
-- 查看 Binlog 保留策略
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
-- MySQL 8.0 使用此参数(替代旧的 expire_logs_days)
-- 建议设为 604800(7 天)
-- 手动清理旧 Binlog
PURGE BINARY LOGS BEFORE '2024-01-01 00:00:00';
PURGE BINARY LOGS TO 'mysql-bin.000010';示例 2:GTID 配置与使用
GTID(Global Transaction Identifier)是 MySQL 5.6 引入的全局事务标识,每个事务有唯一的 GTID,格式为 server_uuid:transaction_id。GTID 简化了复制管理和故障切换。
-- 开启 GTID(my.cnf 配置)
-- [mysqld]
-- gtid_mode = ON
-- enforce_gtid_consistency = ON
-- log_slave_updates = ON
-- 查看 GTID 状态
SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'enforce_gtid_consistency';
SHOW MASTER STATUS;
-- Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-105
-- 查看已执行的 GTID
SELECT @@global.gtid_executed;
-- 使用 GTID 做基于事务的复制
CHANGE MASTER TO
MASTER_HOST='master-host',
MASTER_USER='repl',
MASTER_PASSWORD='repl_pwd',
MASTER_AUTO_POSITION=1; -- 使用 GTID 自动定位,无需手动指定 binlog 位点
-- 从库跳过有问题的 GTID 事务(紧急修复,谨慎使用)
SET GTID_NEXT='3E11FA47-71CA-11E1-9E33-C80AA9429562:106';
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
-- GTID 的优势
-- 1. 不需要手动维护 binlog 文件名和位点
-- 2. 故障切换更简单,直接比较 GTID 集合
-- 3. 可以精确知道哪些事务已执行、哪些未执行
-- 4. 支持 GTID 的在线 DDL(MySQL 8.0)示例 3:Binlog 按时间点恢复(PITR)
PITR(Point-In-Time Recovery)是数据库灾备的核心能力。通过全量备份 + Binlog 增量回放,可以将数据库恢复到任意时间点。
# PITR 完整流程
# 1. 准备全量备份(建议每天凌晨执行)
mysqldump --single-transaction --master-data=2 --all-databases > full_backup_20240115.sql
# --single-transaction:不锁表(InnoDB)
# --master-data=2:记录备份时的 binlog 位点
# 2. 场景:2024-01-15 14:30 发现误删了 orders 表的数据
# 需要恢复到 14:25(误删前的状态)
# 3. 恢复全量备份
mysql -u root -p < full_backup_20240115.sql
# 4. 从全量备份时间点开始回放 Binlog
# 查看备份时的 binlog 位点
head -n 30 full_backup_20240115.sql | grep "MASTER_LOG_FILE\|MASTER_LOG_POS"
# -- MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=45200
# 回放到误删之前
mysqlbinlog --start-position=45200 \
--stop-datetime="2024-01-15 14:25:00" \
mysql-bin.000003 mysql-bin.000004 \
| mysql -u root -p
# 5. 也可以按时间范围恢复
mysqlbinlog --start-datetime="2024-01-15 02:00:00" \
--stop-datetime="2024-01-15 14:25:00" \
mysql-bin.000003 mysql-bin.000004 \
| mysql -u root -p
# 6. 验证数据恢复结果
mysql -u root -p -e "SELECT COUNT(*) FROM orders WHERE created_at < '2024-01-15 14:25:00'"
# 注意事项:
# - 恢复过程中目标库不应有其他写入
# - 如果需要跳过某些事务(如误删),使用 --exclude-gtids
# - 建议先在从库上验证恢复流程示例 4:Canal 消费 Binlog 示例
Canal 是阿里巴巴开源的 CDC 工具,通过伪装 MySQL 从库实时消费 Binlog。
// Canal Client 消费 Binlog 数据
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("canal-server", 11111),
"example", "", "");
connector.connect();
connector.subscribe("my_db\\..*"); // 订阅所有表
connector.rollback(); // 回滚到最新位点
while (true) {
// 批量获取消息,不阻塞
Message message = connector.getWithoutAck(1000);
long batchId = message.getId();
if (batchId == -1 || message.getEntries().isEmpty()) {
Thread.sleep(1000);
continue;
}
for (CanalEntry.Entry entry : message.getEntries()) {
if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) {
continue;
}
CanalEntry.RowChange rowChange =
CanalEntry.RowChange.parseFrom(entry.getStoreValue());
String tableName = entry.getHeader().getTableName();
CanalEntry.EventType eventType = rowChange.getEventType();
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
switch (eventType) {
case INSERT:
// 新数据在 afterColumnsList
for (CanalEntry.Column col : rowData.getAfterColumnsList()) {
System.out.println(col.getName() + " = " + col.getValue());
}
break;
case UPDATE:
// 旧数据在 beforeColumnsList,新数据在 afterColumnsList
break;
case DELETE:
// 被删数据在 beforeColumnsList
break;
}
}
}
connector.ack(batchId); // 确认消费
}示例 5:Debezium CDC 配置(Kafka Connect)
{
"name": "mysql-connector",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.hostname": "mysql-host",
"database.port": "3306",
"database.user": "debezium",
"database.password": "dbz_pwd",
"database.server.id": "184054",
"database.server.name": "my_app",
"database.include.list": "my_db",
"database.history.kafka.bootstrap.servers": "kafka:9092",
"database.history.kafka.topic": "schema-changes.my_app",
"include.schema.changes": "true",
"snapshot.mode": "schema_only"
}
}主从复制架构
主从复制原理:
Master Slave
| |
|--- DML/DDL ---→ |
| Binlog |
| |--- IO Thread 拉取 Binlog
| | → 写入 Relay Log
| |
| |--- SQL Thread 重放 Relay Log
| | → 执行 SQL 写入数据
半同步复制(MySQL 5.7+):
Master Slave
| |
|--- 事务提交 → |
| 写入 Binlog |
|--- 等待 ACK ←------------------|
| Slave 确认收到 Binlog |
|--- 返回提交成功 |
半同步复制保证:
- 至少有一个从库收到了 Binlog
- 如果 Master 崩溃,最多丢失一个事务(而非多个)优点
Binlog 格式深入对比
三种格式详解
-- STATEMENT 格式(SBR)
-- 记录原始 SQL 语句
-- 优点:日志体积小
-- 缺点:
-- - 不确定函数结果不一致(NOW(), UUID(), RAND())
-- - 存储过程/触发器可能产生不同效果
-- - LIMIT 不带 ORDER BY 结果不确定
-- ROW 格式(RBR)— 推荐
-- 记录行变更前后的值
-- 优点:
-- - 每行变更都是确定性的
-- - 不需要 statement-based replication 的一致性问题
-- - 可用于数据审计和恢复
-- 缺点:
-- - 日志体积大(大批量 UPDATE 更严重)
-- - 无法从 Binlog 直接看到执行的 SQL
-- - 不带 WHERE 条件的 UPDATE 会记录所有变更行
-- MIXED 格式(MBR)
-- 自动在 STATEMENT 和 ROW 之间切换
-- 一般语句用 STATEMENT,不确定函数用 ROW
-- 不推荐:行为不可预测,难以排查
-- 查看 Binlog 内容
SHOW BINLOG EVENTS IN 'mysql-bin.000123';
-- 使用 mysqlbinlog 工具
mysqlbinlog --start-datetime="2026-04-14 10:00:00" \
--stop-datetime="2026-04-14 11:00:00" \
--base64-output=DECODE-ROWS -v \
/var/lib/mysql/mysql-bin.000123
-- ROW 格式解码(-v 参数将行事件解码为可读的伪 SQL)
# at 1234
#240414 10:30:15 server id 1 end_log_pos 1500 CRC32 0xabc123
### UPDATE `mydb`.`orders`
### WHERE
### @1=1001
### @2='ORD-20260414-001'
### @3=1
### @4=999.00
### SET
### @3=2 -- status 从 1 变为 2
### @5='2026-04-14 10:30:15'
-- 查看当前 Binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
-- 建议始终使用 ROWBinlog 事件结构
Binlog 文件由一系列事件组成,每个事件包含:
1. 事件头(Event Header)
- timestamp — 事件发生时间
- type_code — 事件类型
- server_id — 产生事件的 server ID
- event_length — 事件总长度
- next_position — 下一个事件的位置
- flags — 事件标志
2. 事件体(Event Body)
- 根据事件类型不同,内容不同
常见事件类型:
FORMAT_DESCRIPTION_EVENT — Binlog 文件头,描述文件格式版本
QUERY_EVENT — STATEMENT 格式的 SQL 语句
TABLE_MAP_EVENT — ROW 格式的表结构映射
WRITE_ROWS_EVENT — ROW 格式的 INSERT
UPDATE_ROWS_EVENT — ROW 格式的 UPDATE
DELETE_ROWS_EVENT — ROW 格式的 DELETE
XID_EVENT — 事务提交事件
GTID_EVENT — GTID 事务标识
STOP_EVENT — Binlog 文件结束
Binlog 文件切换触发条件:
- 文件大小达到 max_binlog_size
- 手动执行 FLUSH LOGS
- MySQL 服务重启Binlog 数据恢复实战
按时间点恢复
# 场景:误删除了 2026-04-14 10:00 到 10:30 之间的数据
# 步骤 1:查找误操作的 Binlog 位置
mysqlbinlog --start-datetime="2026-04-14 09:50:00" \
--stop-datetime="2026-04-14 10:35:00" \
--base64-output=DECODE-ROWS -v \
/var/lib/mysql/mysql-bin.000123 | grep -A 10 "DELETE"
# 步骤 2:找到误操作前的位点
# 假设误操作开始于 position 5000
# 步骤 3:提取恢复 SQL
mysqlbinlog --start-position=4000 --stop-position=5000 \
/var/lib/mysql/mysql-bin.000123 > recovery.sql
# 步骤 4:在恢复库执行
mysql -u root -p recovered_db < recovery.sql
# 步骤 5:反向生成补偿 SQL
# 对于 ROW 格式的 DELETE 事件,生成对应的 INSERT 语句
# 对于 ROW 格式的 UPDATE 事件,生成反向 UPDATE 语句
mysqlbinlog --start-position=5000 --stop-position=5200 \
--base64-output=DECODE-ROWS -v \
/var/lib/mysql/mysql-bin.000123 > rollback.sql
# 步骤 6:验证数据一致性
# 使用 pt-table-checksum 对比主库和恢复库
pt-table-checksum h=localhost --replicate=percona.checksums
pt-table-sync --execute h=localhost --sync-to-masterGTID 恢复
# 使用 GTID 恢复更精确
# 步骤 1:找到误操作的 GTID
mysqlbinlog --start-datetime="2026-04-14 10:00:00" \
--stop-datetime="2026-04-14 10:35:00" \
--include-gtids='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-10000' \
/var/lib/mysql/mysql-bin.000123
# 步骤 2:排除误操作的 GTID
mysqlbinlog --exclude-gtids='3E11FA47-71CA-11E1-9E33-C80AA9429562:9999' \
--include-gtids='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-10000' \
/var/lib/mysql/mysql-bin.000123 > recovery.sql
# 步骤 3:设置 GTID_PURGED 跳过已执行的事务
SET @@GLOBAL.GTID_PURGED='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-9998';
mysql -u root -p < recovery.sqlBinlog 性能影响与优化
Binlog 写入性能
# Binlog 写入路径
# 事务提交 → Binlog Buffer → OS Cache → 磁盘
# sync_binlog 控制刷盘策略
# sync_binlog=0 — 由 OS 决定(最快,可能丢数据)
# sync_binlog=1 — 每次提交刷盘(最安全,性能影响约 15-25%)
# sync_binlog=N — 每 N 次提交刷盘(折中)
# innodb_flush_log_at_trx_commit 配合
# 推荐组合:
# sync_binlog=1 + innodb_flush_log_at_trx_commit=1 — 双1,最安全
# sync_binlog=0 + innodb_flush_log_at_trx_commit=0 — 最快,可能丢数据
# sync_binlog=1 + innodb_flush_log_at_trx_commit=2 — 折中
# binlog_cache_size — 每个连接的 Binlog 缓冲区
binlog_cache_size = 128K
# 如果事务超过这个大小,会使用临时文件
# max_binlog_cache_size — 最大缓存大小
max_binlog_cache_size = 4G
# 超过这个值会报错:Multi-statement transaction required more than max_binlog_cache_size
# binlog_row_image — ROW 格式的记录策略(MySQL 5.6+)
# FULL — 记录所有列(默认)
# MINIMAL — 只记录被修改的列和标识列(推荐,减少日志体积)
# NOBLOB — 不记录 BLOB/TEXT 列
binlog_row_image = MINIMAL
# 性能对比:
# FULL: 每行 UPDATE 记录所有列
# MINIMAL: 只记录修改的列,Binlog 体积可减少 30-70%Binlog 空间管理
-- 查看 Binlog 文件列表
SHOW BINARY LOGS;
SHOW MASTER STATUS;
-- 手动清理
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);
PURGE BINARY LOGS TO 'mysql-bin.000120';
-- MySQL 8.0 配置自动清理
SET GLOBAL binlog_expire_logs_seconds = 604800; -- 7 天
-- 查看 Binlog 磁盘使用
SELECT
file_name,
file_size / 1024 / 1024 AS size_mb,
TIMESTAMP(date) AS create_time
FROM mysql.master_log_status
ORDER BY create_time DESC;
-- Linux 命令查看
ls -lh /var/lib/mysql/mysql-bin.*
du -sh /var/lib/mysql/mysql-bin.*
-- 监控脚本
# 检查 Binlog 总大小
BINLOG_SIZE=$(du -sb /var/lib/mysql/mysql-bin.* | awk '{sum += $1} END {print sum}')
DISK_AVAIL=$(df --output=avail /var/lib/mysql | tail -1)
THRESHOLD=$((DISK_AVAIL / 5)) -- 磁盘剩余不足 20% 时告警
if [ $BINLOG_SIZE -gt $THRESHOLD ]; then
echo "WARNING: Binlog using too much disk space"
fi复制过滤与延迟监控
复制过滤配置
# 主库过滤(只发送特定库/表)
# 注意:GTID 模式下不推荐使用复制过滤
[mysqld]
binlog-do-db = myapp -- 只复制 myapp 库
binlog-ignore-db = mysql -- 不复制 mysql 库
binlog-ignore-db = information_schema
# 从库过滤(只重放特定库/表)
[mysqld]
replicate-do-db = myapp
replicate-do-table = myapp.orders
replicate-do-table = myapp.users
replicate-ignore-table = myapp.logs
replicate-wild-do-table = myapp.important_% -- 通配符匹配延迟监控与优化
-- 查看从库延迟
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 延迟秒数
-- 注意:这个值可能不准确(如果 IO Thread 停了但 SQL Thread 还在追)
-- 更准确的延迟计算(对比主库和从库的位点)
-- 主库
SHOW MASTER STATUS;
-- File: mysql-bin.000123, Position: 5000
-- 从库
SHOW SLAVE STATUS\G
-- Relay_Master_Log_File: mysql-bin.000123, Exec_Master_Log_Pos: 4800
-- 实际延迟 = 5000 - 4800 = 200 个事件
-- 并行复制优化(MySQL 5.7+)
[mysqld]
slave_parallel_workers = 8 -- 并行 worker 数(建议 4-8)
slave_parallel_type = LOGICAL_CLOCK -- 按组提交并行
slave_preserve_commit_order = 1 -- 保持提交顺序
-- MySQL 8.0 增强并行复制
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK
binlog_transaction_dependency_tracking = WRITESET -- WRITESET 模式更精确
-- 延迟优化策略:
-- 1. 增加并行 worker 数量
-- 2. 使用 WRITESET 事务依赖跟踪
-- 3. 从库使用 SSD 存储
-- 4. 减少大事务(分批操作)
-- 5. 避免从库上的长查询干扰 SQL Thread缺点
总结
Binlog 是 MySQL 数据流转的枢纽,主从复制、CDC、PITR 都依赖它。生产环境应使用 ROW 格式、开启 GTID、配置合理的保留策略和刷盘参数,并建立从库延迟和 Binlog 磁盘空间的监控告警。
关键知识点
sync_binlog=1保证每次提交都刷盘,是最安全但最慢的设置- ROW 格式下 Binlog 记录行变更前后值,STATEMENT 格式只记录 SQL 语句
- GTID 格式:
server_uuid:transaction_id,全局唯一,简化复制拓扑管理 binlog_expire_logs_seconds(MySQL 8.0)控制 Binlog 保留天数
项目落地视角
- 生产环境必开 Binlog,格式为 ROW,配合
sync_binlog=1 - 设置 Binlog 保留 7-14 天,监控磁盘使用率
- CDC 场景优先使用 Debezium + Kafka,Canal 在阿里生态中更成熟
- 定期演练 PITR 流程:全量备份 → Binlog 回放 → 验证数据一致性
常见误区
- 认为 STATEMENT 格式更省空间:但某些函数(NOW()、USER())在从库执行结果不一致
- 忽略 Binlog 磁盘空间:大量写入场景下 Binlog 增长很快,可能撑满磁盘
- 不监控从库延迟:Seconds_Behind_Master 只反映 SQL Thread 延迟,不反映 IO Thread
- 修改 Binlog 格式不重启:
binlog_format是 SESSION 级别可改,但已有连接仍用旧格式
进阶路线
- 学习 MySQL 半同步复制和 Group Replication 的实现原理
- 研究并行复制:MySQL 5.7 的基于组提交的并行复制(LOGICAL_CLOCK)
- 了解 MHA、Orchestrator 等 MySQL 高可用故障切换工具
- 学习 Flink CDC 2.x 的无锁读取和增量快照算法
适用场景
- 主从复制、读写分离架构的数据同步
- 基于 CDC 的数据管道:MySQL → Kafka → Elasticsearch/ClickHouse
- 按时间点恢复(PITR)灾难恢复方案
落地建议
- GTID + ROW 格式是现代 MySQL 的标配,新项目直接启用
- 全量备份每天一次,Binlog 保留至少 7 天
- 监控从库延迟、Binlog 磁盘使用率、GTID 一致性
排错清单
SHOW MASTER STATUS确认 Binlog 是否开启、当前位点SHOW SLAVE STATUS\G检查从库复制状态、延迟秒数、错误信息SHOW BINARY LOGS检查 Binlog 文件数量和大小
复盘问题
- Binlog 保留策略是什么?磁盘空间是否够用?
- 最近一次主从切换花了多久?GTID 是否一致?
- CDC 管道的消费延迟是多少?异常重启后位点如何恢复?
