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的总体结构

先看一张我画的整体结构图,帮你建立全局印象:

ROW格式三种Rows Event结构对比 ROW格式三种Rows Event结构对比 INSERT (Write_rows_event) Table ID + 列元信息 标识是哪张表 After Image 新插入的完整行数据 无 Before Image (新行没有旧值) 行数 ≥ 1 UPDATE (Update_rows_event) Table ID + 列元信息 标识是哪张表 Before Image 变更前的行数据 After Image 变更后的行数据 行数 ≥ 1 DELETE (Delete_rows_event) Table ID + 列元信息 标识是哪张表 Before Image 被删除的完整行数据 无 After Image (删除后没有新值) 行数 ≥ 1 注意:每个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 解析流程总结

最后,我整理了一个通用的解析流程,你可以直接参考:

  1. 读取Event头:获取Event类型(INSERT/UPDATE/DELETE)和Table ID
  2. 查找表元信息:根据Table ID找到对应的表结构(列名、类型、顺序)
  3. 解析行数据:根据Event类型,读取Before Image和/或After Image
  4. 按列解码:将二进制数据按列类型解码为可读的值
  5. 构建变更记录:输出结构化的变更数据(如JSON格式)
  6. 逐行处理:如果Event包含多行,循环执行步骤3-5

核心要点:ROW格式的Binlog解析,本质上就是“按表结构解码二进制行数据”。INSERT只有新值,DELETE只有旧值,UPDATE新旧都有。理解了这个,你就掌握了CDC数据捕获的基石。