数据格式解析:Debezium 事件结构、Before/After 快照、Source 元数据、op 字段含义
好,咱们进入第四章。前面几章我们把 Debezium 的部署和基本配置跑通了,现在该聊聊最核心的东西——数据长什么样。
说白了,你费了半天劲把 Debezium 搭起来,最终消费端拿到的是一个 JSON 消息。这个 JSON 里到底有什么?哪些字段是必须关注的?哪些是坑?今天一次性讲清楚。
Debezium 事件的整体骨架
每个 Debezium 事件,本质上是一个结构化的 JSON 对象。我习惯把它拆成三层来看:
- 外层信封(Envelope):包含操作类型、时间戳等元信息。
- 中间负载(Payload):真正的数据变化部分,包括 before、after、source。
- 内层细节:字段级别的值,以及对应的 schema 信息(如果开启)。
一个典型的 MySQL Binlog 事件,简化后大概长这样:
{
"schema": { ... },
"payload": {
"before": null,
"after": {
"id": 1001,
"name": "张三",
"balance": 5000.00
},
"source": {
"version": "1.9.7.Final",
"connector": "mysql",
"name": "mysql-server-1",
"ts_ms": 1695000000000,
"snapshot": "false",
"db": "bank",
"table": "accounts",
"server_id": 223344,
"gtid": null,
"file": "mysql-bin.000023",
"pos": 45678,
"row": 0,
"thread": 12345
},
"op": "c",
"ts_ms": 1695000000123,
"transaction": null
}
}
嗯,看着有点长。别急,我们拆开一个一个讲。
op 字段:操作类型,事件的灵魂
op 字段是每个事件里最重要的字段之一。它告诉你数据库里发生了什么操作。我个人习惯在消费端第一件事就是判断 op,然后走不同的处理逻辑。
| op 值 | 含义 | 说明 |
|---|---|---|
c |
Create(创建) | 对应 INSERT 操作,after 里有新数据 |
u |
Update(更新) | 对应 UPDATE 操作,before 和 after 都有数据 |
d |
Delete(删除) | 对应 DELETE 操作,before 里有被删的数据 |
r |
Read(读取) | 仅在初始快照阶段出现,表示一条存量数据 |
关键点:op: "r" 只在快照阶段出现。如果你做实时同步,通常需要区分快照事件和增量事件,避免重复处理。
我曾经在一个项目里踩过坑——消费端没判断 op,直接把所有事件都当增量处理,结果快照阶段的数据被重复写入了目标库。嗯,从那以后我养成了习惯:先看 op,再决定怎么处理。
Before 和 After:数据变化的快照
这两个字段是事件的核心负载。它们分别代表:
- before:操作发生前的数据行。对于 INSERT,before 为 null;对于 UPDATE 和 DELETE,before 包含旧数据。
- after:操作发生后的数据行。对于 INSERT 和 UPDATE,after 包含新数据;对于 DELETE,after 为 null。
你想想看,有了 before 和 after,你就能精确知道每一行数据的变化轨迹。这在做数据审计、回滚、或者同步到搜索引擎时特别有用。
小技巧:如果你只需要最终状态,可以只关注 after 字段。但如果你需要做变更数据捕获(CDC)的完整链路,before 和 after 都要保留。
Source 元数据:事件的身份证
source 字段包含了这条数据变更的来源信息。说白了,就是告诉你这条数据是从哪个数据库、哪个表、哪个 Binlog 位置来的。
我重点讲几个常用的:
- db / table:数据库名和表名。如果你监听多个表,这两个字段用来区分数据来源。
- ts_ms:事件在数据库端产生的时间戳(毫秒级)。注意,这是数据库的时间,不是 Debezium 处理的时间。
- file / pos:Binlog 文件名和偏移量。这是定位事件的精确坐标,做断点续传时必用。
- snapshot:布尔值,标记当前事件是否来自快照阶段。如果是
true,说明是存量数据;如果是false,说明是实时增量。 - server_id:产生该事件的 MySQL 服务器 ID。在主从架构下,可以用来区分数据来自哪个节点。
注意:ts_ms 是数据库提交事务的时间,不是 Debezium 捕获到事件的时间。如果你需要计算端到端延迟,应该用事件本身的 ts_ms(外层那个)减去 source.ts_ms。
Schema 信息:要不要开?
Debezium 默认会在每个事件里带上 schema 字段,描述每个字段的类型、是否可选等信息。这在某些场景下很有用,比如动态生成目标表的 DDL。
但说实话,我在大部分项目里都选择关闭 schema 输出。原因很简单:
- 它让事件体积膨胀了 2-3 倍。
- 大多数消费端(比如写 Elasticsearch、Kafka Sink)不需要 schema 信息。
- 如果表结构变了,schema 版本管理反而更复杂。
关闭方式是在连接器配置里加一行:
"key.converter.schemas.enable": false,
"value.converter.schemas.enable": false
当然,如果你需要做 Schema Registry 之类的强类型校验,那就另当别论了。
事件结构全景图
为了让你更直观地理解整个事件结构,我画了一张图:
实战中的注意事项
讲完了结构,我分享几个实际项目中容易踩的坑。
1. 字段名大小写问题
MySQL 在 Linux 下默认区分大小写,但 Debezium 输出的 JSON 字段名默认是小写的。如果你数据库里用了大写字段名,消费端要注意做映射。
2. 时间字段的时区
ts_ms 是 UTC 时间戳。我曾经遇到一个情况:消费端直接用这个时间戳做展示,结果发现时间对不上。后来才意识到需要转成业务时区。
3. 大字段的处理
如果表里有 TEXT、BLOB 类型的字段,Debezium 默认会输出完整内容。对于大字段,建议在连接器配置里设置 column.mask.with.* 或 column.truncate.to.* 来限制大小,否则事件体积会非常大。
4. 空值的表示
MySQL 中的 NULL 在 JSON 里就是 null。但要注意,有些字段在 before 和 after 里可能同时为 null,这表示该字段没有被修改。
总结一下:Debezium 事件结构其实不复杂,核心就是 op + before + after + source。把这四个字段搞明白,你就能应对 90% 的场景了。