MySQL Binlog 核心原理(上):Binlog 的三种格式详解与对比

聊到 Debezium,绕不开 Binlog。说白了,Binlog 就是 MySQL 的“黑匣子”,记录了所有数据变更。Debezium 正是靠读取这个黑匣子,才能实时捕获数据变化。

但 Binlog 有三种格式:STATEMENT、ROW、MIXED。选错了,轻则性能下降,重则数据不一致。我见过不少团队,因为格式选错,导致数据同步出问题,最后不得不全量重刷。今天咱们就把这三种格式掰开揉碎,讲清楚。

Binlog 是什么?

Binlog,全称 Binary Log,二进制日志。它记录了 MySQL 数据库所有数据变更操作,比如 INSERT、UPDATE、DELETE,以及表结构变更(DDL)。

注意,SELECT 查询不会记录。为什么?因为查询不改变数据,记了也没意义。

Binlog 的主要用途有两个:

  • 主从复制:从库读取主库的 Binlog,重放操作,保持数据一致。
  • 数据恢复:通过 Binlog 回滚到某个时间点,做 point-in-time recovery。

Debezium 做的事情,其实和从库类似——它把自己伪装成一个 MySQL 从库,读取 Binlog,然后把变更事件推给 Kafka。

三种格式概览

MySQL 提供了三种 Binlog 格式,咱们先看一张对比表,心里有个底。

格式 记录内容 优点 缺点
STATEMENT 执行的 SQL 语句 日志量小,易于理解 依赖上下文,可能数据不一致
ROW 每行数据的具体变更 精确、安全、无歧义 日志量大,尤其大事务
MIXED 混合模式,自动选择 兼顾两者优点 逻辑复杂,不可控

STATEMENT 格式:记录 SQL 语句

STATEMENT 格式,顾名思义,记录的是原始 SQL 语句。比如你执行了:

UPDATE users SET age = age + 1 WHERE id = 1;

Binlog 里就原样记下这条 SQL。从库拿到后,再执行一遍。

优点很明显:日志量小。一条 SQL 可能影响几千行,但日志里就一行文本。磁盘占用少,网络传输也快。

但坑也不少。我举个例子,你执行:

DELETE FROM users ORDER BY id LIMIT 1;

这条 SQL 在主库执行时,删的是 id 最小的那条。但从库的数据分布可能不同,ORDER BY 的结果不一样,删的就是另一条了。数据就乱了。

还有,像 NOW()、UUID()、RAND() 这些非确定性函数,每次执行结果都不同。STATEMENT 格式下,从库重放时拿到的值跟主库不一样。

注意:STATEMENT 格式在 MySQL 5.7 之后已不推荐使用。如果你还在用,建议尽快迁移。Debezium 也不支持 STATEMENT 格式,因为它无法从 SQL 语句中提取精确的行变更数据。

ROW 格式:记录行变更

ROW 格式,记录的是每一行数据的具体变化。比如你更新了一行,Binlog 里会记下:

  • 更新前这行长什么样(before image)
  • 更新后这行长什么样(after image)

用 mysqlbinlog 工具查看,大概长这样:

### UPDATE `test`.`users`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2='张三' /* STRING meta=65060 nullable=0 is_null=0 */
### SET
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2='李四' /* STRING meta=65060 nullable=0 is_null=0 */

看到没?它明确告诉你:原来 id=1 的 name 是 '张三',现在变成了 '李四'。没有任何歧义。

这就是 Debezium 选择 ROW 格式的原因。Debezium 需要精确知道每一行的变更内容,才能生成正确的 CDC 事件。STATEMENT 格式做不到这一点。

但 ROW 格式也有代价。如果你一次更新 10 万行,Binlog 里就会记录 10 万条行变更。日志量巨大。我遇到过一个大事务,Binlog 直接写了几个 GB,把磁盘撑爆了。

经验之谈:我曾经在一个订单系统里,因为一次批量更新操作,Binlog 暴涨到 5GB。从那以后,我建议团队对大事务做拆分,每次更新不超过 1000 行。既保护了 Binlog,也减少了锁冲突。

MIXED 格式:自动切换

MIXED 格式,是 MySQL 的“智能模式”。它默认使用 STATEMENT 格式,但遇到非确定性语句时,自动切换为 ROW 格式。

比如:

  • 普通 INSERT、UPDATE 用 STATEMENT
  • 遇到 NOW()、UUID()、LIMIT 子句时,自动切 ROW

听起来很完美,对吧?但实际用起来,问题不少。

首先,不可控。你无法预知 MySQL 什么时候会切换格式。同一个 SQL,在不同版本、不同配置下,行为可能不一样。

其次,混合格式对 Debezium 不友好。Debezium 需要统一的 ROW 格式来解析。如果 Binlog 里混着 STATEMENT 事件,Debezium 会直接跳过,导致数据丢失。

结论:对于 Debezium 场景,必须使用 ROW 格式。这是硬性要求,没有商量余地。

三种格式的对比分析

咱们再深入对比一下,看看每种格式在关键维度上的表现。

对比维度 STATEMENT ROW MIXED
日志大小 小 大(尤其批量操作) 中等
数据一致性 低(非确定性函数) 高(精确记录行) 中等
可读性 高(直接看 SQL) 低(二进制格式) 混合
Debezium 支持 不支持 必须 不推荐
主从复制安全性 低 高 中等
磁盘 I/O 压力 低 高 中等

如何查看和修改 Binlog 格式

查看当前 Binlog 格式:

SHOW VARIABLES LIKE 'binlog_format';

修改 Binlog 格式(需要重启 MySQL 或动态修改):

-- 动态修改(立即生效,但重启后失效)
SET GLOBAL binlog_format = 'ROW';

-- 永久修改(修改 my.cnf 配置文件)
[mysqld]
binlog_format = ROW
注意:动态修改 binlog_format 只对新连接生效,已有连接不受影响。建议在低峰期操作,并重启所有应用连接。

ROW 格式的额外配置

既然 Debezium 必须用 ROW 格式,那咱们还得了解几个相关参数。

binlog_row_image:控制 ROW 格式记录哪些列。

  • FULL:记录所有列(默认值)
  • MINIMAL:只记录主键和被修改的列
  • NOBLOB:记录所有列,但排除 BLOB/TEXT 列

我个人习惯用 MINIMAL。为什么?因为日志量更小,网络传输更快。Debezium 也能正常工作,它只需要主键和变更列就够了。

设置方法:

[mysqld]
binlog_format = ROW
binlog_row_image = MINIMAL

binlog_rows_query_log_events:是否在 ROW 格式的 Binlog 中记录原始 SQL。默认关闭。开启后,Debezium 可以获取到原始 SQL,方便调试。

[mysqld]
binlog_rows_query_log_events = ON

SVG 图:Binlog 三种格式对比

下面这张图,帮你直观理解三种格式的区别。

Binlog 三种格式对比 STATEMENT 记录 SQL 语句 日志量小 数据一致性低 Debezium 不支持 不推荐使用 ROW 记录行变更 日志量大 数据一致性高 Debezium 必须 推荐使用 MIXED 自动切换 日志量中等 数据一致性中等 Debezium 不推荐 不可控 核心结论 Debezium 必须使用 ROW 格式 STATEMENT 格式已不推荐 MIXED 格式不可控,不推荐用于 CDC 场景 建议配置:binlog_format=ROW + binlog_row_image=MINIMAL

避坑指南

最后,分享几个我踩过的坑。

坑一:Binlog 格式改了,但应用没重启

我曾经在线上动态修改了 binlog_format,以为万事大吉。结果发现 Debezium 还是读不到 ROW 事件。查了半天,原来动态修改只对新连接生效,已有的 Debezium 连接还在用 STATEMENT。重启连接后解决。

坑二:ROW 格式下,大事务撑爆磁盘

有一次,一个批量更新操作,一次改了 50 万行。Binlog 直接写了 3GB,磁盘告警。从那以后,我强制团队所有批量操作必须分页,每次不超过 1000 行。

坑三:binlog_row_image 设置不当,导致数据丢失

有同事把 binlog_row_image 设成了 MINIMAL,但 Debezium 的某些版本需要 FULL 才能获取完整的前后镜像。结果同步到 Kafka 的数据缺少部分列。升级 Debezium 版本后解决。

建议:如果你刚开始用 Debezium,直接上 ROW 格式 + MINIMAL 镜像。这是最稳妥的组合。等跑熟了,再根据业务需求调整。