4、ROW格式下的数据变更解析:INSERT、UPDATE、DELETE对应的Rows Event
说实话,很多做CDC的同学一开始就被Binlog的ROW格式吓住了。觉得二进制、十六进制、各种Event类型,太复杂。但在我看来,ROW格式反而是最直观的——它直接把数据变更的“前后快照”摆在你面前。
我个人习惯把ROW格式的Binlog比作“数据库的CT扫描”。STATEMENT格式只告诉你“做了什么操作”,而ROW格式告诉你“哪几行数据,从什么值变成了什么值”。今天我们就来拆解这三个核心Event:Write_rows_event(INSERT)、Update_rows_event(UPDATE)、Delete_rows_event(DELETE)。
核心认知:ROW格式下,每个Rows Event都包含一个或多个“行变更”的集合。每个行变更都携带了before image(变更前镜像)和/或after image(变更后镜像)。这是CDC数据还原的基石。
4.1 三种Rows Event的总体结构
先看一张我画的整体结构图,帮你建立全局印象:
这张图你看懂了吗?INSERT只有After Image,DELETE只有Before Image,UPDATE两者都有。这个差异决定了你在解析时如何提取数据。
4.2 INSERT:Write_rows_event 解析
INSERT操作在Binlog里对应的是Write_rows_event。说白了,就是告诉CDC消费者:“我在某张表里新插入了N行数据,这是它们现在的样子。”
解析时,你只需要关注After Image。每一行After Image都包含该行所有列的值,按照表定义时的列顺序排列。
来看一个实际例子。假设我们有张用户表:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
执行一条INSERT:
INSERT INTO user (name, age, created_at) VALUES ('张三', 28, NOW());
Binlog中对应的Write_rows_event解析后,After Image大致是这样的结构:
| 列名 | 类型 | 值 | 说明 |
|---|---|---|---|
| id | int | 1 | 自增主键,自动生成 |
| name | varchar | 张三 | 字符串,长度可变 |
| age | int | 28 | 整数 |
| created_at | datetime | 2024-01-15 10:30:00 | 日期时间 |
个人经验:解析INSERT时,我建议直接拿After Image的完整行数据去构建目标数据。但要注意——如果表有默认值或自增列,Binlog里会包含这些值,不需要你额外计算。我曾经遇到一个坑:有人以为自增ID在Binlog里是0或NULL,结果同步到目标库时主键冲突了。实际上Binlog里写的是最终入库的值。
4.3 UPDATE:Update_rows_event 解析
UPDATE操作对应Update_rows_event。这是三种Event里信息量最大的,因为它同时携带了Before Image和After Image。
为什么需要两个镜像?你想想看——如果你只拿到After Image,你根本不知道哪行数据被改了。Before Image就是用来定位原始行的。在CDC场景下,Before Image通常用于:
- 数据校验:确认原始数据确实是你期望的值
- 增量更新:在目标库中找到对应的行进行更新
- 审计日志:记录“从什么值变成了什么值”
继续用user表举例。执行一条UPDATE:
UPDATE user SET age = 29 WHERE id = 1;
解析后的Update_rows_event包含两套数据:
| 列名 | Before Image | After Image | 是否变更 |
|---|---|---|---|
| id | 1 | 1 | 否(主键未变) |
| name | 张三 | 张三 | 否 |
| age | 28 | 29 | 是 |
| created_at | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 | 否 |
注意:Binlog中Before Image和After Image的列顺序完全一致。但有个细节——如果表没有定义主键,MySQL会使用所有列作为“隐式主键”来定位行。这时候Before Image会包含所有列的值,即使只有一列被修改。我曾经在线上遇到过一张没有主键的日志表,UPDATE时Binlog体积暴涨了3倍,就是因为Before Image包含了全部列。
4.4 DELETE:Delete_rows_event 解析
DELETE操作对应Delete_rows_event。它只包含Before Image,没有After Image。为什么?因为行都被删了,哪来的新值?
解析DELETE时,你拿到的就是被删除行的完整快照。这个快照在CDC中非常有用——你可以用它来:
- 在目标库中删除对应的行
- 将删除操作记录到审计表或归档表
- 触发级联清理逻辑
执行一条DELETE:
DELETE FROM user WHERE id = 1;
解析后的Delete_rows_event只有Before Image:
| 列名 | Before Image | 说明 |
|---|---|---|
| id | 1 | 被删除行的主键 |
| name | 张三 | 被删除行的name |
| age | 29 | 被删除行的age |
| created_at | 2024-01-15 10:30:00 | 被删除行的创建时间 |
避坑指南:我曾经在解析DELETE时犯过一个错误——以为Before Image只包含主键列。实际上,binlog_row_image参数控制着镜像的完整度。默认是FULL,即所有列都包含。如果设置为MINIMAL,则Before Image只包含主键列和唯一键列。这个参数在生产环境中经常被忽略,但影响巨大。我建议你在解析时先检查这个参数,否则可能拿不到完整的删除数据。
4.5 批量操作与多行Event
一个Rows Event可以包含多行变更。比如你执行:
UPDATE user SET age = age + 1 WHERE age < 30;
如果影响了100行,Binlog里只会生成一个Update_rows_event,但这个Event内部包含100组Before Image + After Image对。解析时你需要遍历这个列表,逐行处理。
批量操作时,Event的大小可能非常大。我记得有一次处理一个批量DELETE操作,一个Delete_rows_event包含了50万行数据,直接导致解析程序内存溢出。后来我加了一个逐行流式处理的逻辑,才解决了这个问题。
4.6 解析流程总结
最后,我整理了一个通用的解析流程,你可以直接参考:
- 读取Event头:获取Event类型(INSERT/UPDATE/DELETE)和Table ID
- 查找表元信息:根据Table ID找到对应的表结构(列名、类型、顺序)
- 解析行数据:根据Event类型,读取Before Image和/或After Image
- 按列解码:将二进制数据按列类型解码为可读的值
- 构建变更记录:输出结构化的变更数据(如JSON格式)
- 逐行处理:如果Event包含多行,循环执行步骤3-5
核心要点:ROW格式的Binlog解析,本质上就是“按表结构解码二进制行数据”。INSERT只有新值,DELETE只有旧值,UPDATE新旧都有。理解了这个,你就掌握了CDC数据捕获的基石。