数据格式解析: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 之类的强类型校验,那就另当别论了。

事件结构全景图

为了让你更直观地理解整个事件结构,我画了一张图:

Debezium 事件结构全景图 事件信封(Envelope) schema 字段类型、是否可选、版本 payload(负载) before 操作前的数据行 INSERT 时为 null after 操作后的数据行 DELETE 时为 null source 来源元数据 db/table/file/pos op c/u/d/r ts_ms 事件时间戳 transaction 事务元信息 说明:schema 可关闭以减小事件体积;before/after 用于精确追踪数据变化轨迹 source 中的 file/pos 是断点续传的关键坐标

实战中的注意事项

讲完了结构,我分享几个实际项目中容易踩的坑。

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% 的场景了。