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 格式下,从库重放时拿到的值跟主库不一样。
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,把磁盘撑爆了。
MIXED 格式:自动切换
MIXED 格式,是 MySQL 的“智能模式”。它默认使用 STATEMENT 格式,但遇到非确定性语句时,自动切换为 ROW 格式。
比如:
- 普通 INSERT、UPDATE 用 STATEMENT
- 遇到
NOW()、UUID()、LIMIT子句时,自动切 ROW
听起来很完美,对吧?但实际用起来,问题不少。
首先,不可控。你无法预知 MySQL 什么时候会切换格式。同一个 SQL,在不同版本、不同配置下,行为可能不一样。
其次,混合格式对 Debezium 不友好。Debezium 需要统一的 ROW 格式来解析。如果 Binlog 里混着 STATEMENT 事件,Debezium 会直接跳过,导致数据丢失。
三种格式的对比分析
咱们再深入对比一下,看看每种格式在关键维度上的表现。
| 对比维度 | 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 格式改了,但应用没重启
我曾经在线上动态修改了 binlog_format,以为万事大吉。结果发现 Debezium 还是读不到 ROW 事件。查了半天,原来动态修改只对新连接生效,已有的 Debezium 连接还在用 STATEMENT。重启连接后解决。
坑二:ROW 格式下,大事务撑爆磁盘
有一次,一个批量更新操作,一次改了 50 万行。Binlog 直接写了 3GB,磁盘告警。从那以后,我强制团队所有批量操作必须分页,每次不超过 1000 行。
坑三:binlog_row_image 设置不当,导致数据丢失
有同事把 binlog_row_image 设成了 MINIMAL,但 Debezium 的某些版本需要 FULL 才能获取完整的前后镜像。结果同步到 Kafka 的数据缺少部分列。升级 Debezium 版本后解决。