3、Flink CDC 核心原理:Debezium内核机制、Snapshot与Streaming阶段、Changelog Stream概念、Exactly-Once语义保证

这一章,我们来啃硬骨头。Flink CDC 之所以强大,底层站着两个巨人:一个是 Debezium,一个是 Flink 自身的状态机制。说白了,CDC 的实时同步能力,就是靠 Debezium 抓取数据库变更,再靠 Flink 的 Changelog Stream 和 Exactly-Once 语义来保证数据不丢不重。

我刚开始接触 CDC 时,也困惑过:Debezium 到底是怎么工作的?Snapshot 和 Streaming 是什么关系?Changelog Stream 又是个什么概念?别急,我们一个一个拆开来看。

3.1 Debezium 内核机制:CDC 的“眼睛”

Debezium 是一个基于 Kafka Connect 的分布式 CDC 平台。它的核心任务,就是监听数据库的变更日志(比如 MySQL 的 binlog、PostgreSQL 的 WAL),然后把每一次插入、更新、删除操作,转换成结构化的消息事件。

我个人习惯把 Debezium 比作数据库的“监控摄像头”。它不直接查询表,而是读取数据库内部的事务日志。这样做的好处很明显:对源库几乎没有性能影响。

核心要点:Debezium 的每个连接器(Connector)会为每一张表生成一个独立的事件流。每个事件都包含完整的 before/after 镜像,以及操作类型(c = create, u = update, d = delete, r = snapshot read)。

举个例子,MySQL 的一条 UPDATE 语句,Debezium 捕获后会生成类似这样的 JSON 结构:

{
  "op": "u",
  "before": {"id": 1, "name": "Alice", "balance": 100},
  "after":  {"id": 1, "name": "Alice", "balance": 200},
  "source": {
    "db": "mydb",
    "table": "users",
    "ts_ms": 1700000000000
  }
}

嗯,这里要注意:Debezium 默认会保留变更前后的完整数据行。这在做数据回溯或审计时特别有用。我在项目中遇到过一个问题——某个业务需要回滚到 30 分钟前的状态,幸好 Debezium 保留了 before 镜像,我们直接回放事件就搞定了。

3.2 Snapshot 与 Streaming 阶段:两个阶段,一个使命

Debezium 的工作流程分为两个阶段:Snapshot(快照)和Streaming(流式)。这两个阶段是顺序执行的,但逻辑上完全独立。

3.2.1 Snapshot 阶段:先拍一张“全家福”

当连接器第一次启动时,它并不知道数据库里已经有哪些数据。所以,Debezium 会先执行一次全量快照——把表里的所有现有数据读出来,每条记录生成一个类型为 r(snapshot read)的事件。

Snapshot 阶段有几个关键特性:

  • 默认使用 SELECT 语句:Debezium 会对每张表执行 SELECT * FROM table,然后逐行发送事件。
  • 支持断点续传:如果快照过程中连接器挂了,重启后会从上次中断的位置继续,而不是重新扫全表。
  • 快照模式可配置:你可以选择 initial(先快照再流式)、when_needed(仅在需要时快照)、never(跳过快照,只从 binlog 当前位置开始)。

我曾经踩过的坑:有一次在生产环境启动 CDC 任务,忘了配置快照模式。默认的 initial 模式直接扫了全表,导致源库 CPU 飙升到 90%。后来我学乖了——对大表(千万级以上)建议先用 when_needed 模式,或者手动控制快照的并发度。

3.2.2 Streaming 阶段:持续监听变更

快照完成后,Debezium 自动切换到 Streaming 阶段。它会连接到数据库的变更日志流(比如 MySQL 的 binlog),实时捕获后续的每一次数据变更。

Streaming 阶段的核心机制:

  • 基于偏移量(Offset)管理进度:Debezium 会定期把当前读取到的 binlog 位置(文件名 + 偏移量)写入 Kafka Connect 的 offset 存储中。
  • 低延迟:从数据库提交事务到 Debezium 发出事件,延迟通常在毫秒级别。
  • 容错:如果连接器挂了,重启后会从上次记录的 offset 位置继续读取,不会丢失数据。

你想想看,这两个阶段配合起来,就实现了“先全量、后增量”的完整数据同步。这也是 Flink CDC 能同时处理历史数据和实时数据的原因。

3.3 Changelog Stream 概念:Flink 眼中的“变更流”

Debezium 把数据库变更事件发到 Kafka,Flink CDC 连接器消费这些事件后,会将其转换成 Flink 内部的 Changelog Stream。这是一个非常重要的概念。

Changelog Stream 本质上是一个带有操作类型标记的数据流。Flink 用 RowKind 枚举来表示四种操作:

RowKind 含义 对应 Debezium op
INSERT 插入新行 c
UPDATE_BEFORE 更新前的旧行 u 的 before
UPDATE_AFTER 更新后的新行 u 的 after
DELETE 删除的行 d

为什么要区分 UPDATE_BEFORE 和 UPDATE_AFTER?因为 Flink 需要知道“哪一行被替换了”,才能在状态后端中正确地更新或删除对应的数据。如果没有 UPDATE_BEFORE,Flink 就无法确定要覆盖哪一行——尤其是在非主键表或复杂 join 场景下。

我的经验:在写 Flink SQL 做 CDC 同步时,如果你用 CREATE TABLE ... WITH ('connector' = 'mysql-cdc'),Flink 会自动把 Debezium 事件转换成 Changelog Stream。你不需要手动处理 RowKind。但如果你用 DataStream API 消费 Debezium 的 JSON 消息,那就得自己解析 op 字段并设置 RowKind 了。

下面这张图展示了从数据库变更到 Changelog Stream 的完整链路:

Flink CDC 数据流转链路 Flink CDC 数据流转链路 数据库 Binlog / WAL Debezium Kafka Topic Flink CDC Changelog Stream 说明:数据库变更 → Binlog → Debezium 捕获 → Kafka 存储 → Flink 消费 → 转为 Changelog Stream Changelog Stream 包含 RowKind(INSERT / UPDATE_BEFORE / UPDATE_AFTER / DELETE)

3.4 Exactly-Once 语义保证:不丢不重,刚刚好

实时数据同步最怕什么?数据丢了,或者重复了。Flink CDC 结合 Debezium 和 Flink 的 checkpoint 机制,提供了 Exactly-Once 语义保证。说白了,就是每条数据只会被处理一次,不多不少。

这个保证是怎么实现的?核心在于两点:

  1. Debezium 的 offset 管理:Debezium 把当前读取到的 binlog 位置记录在 Kafka Connect 的 offset 中。这个 offset 是单调递增的,不会回退。
  2. Flink 的 checkpoint + 两阶段提交:Flink 定期做 checkpoint,把当前的状态(包括已读取的 offset 和中间计算结果)持久化到状态后端。如果任务挂了,重启后会从最近一次成功的 checkpoint 恢复,然后重新读取从那个 offset 开始的数据。

关键机制:Flink CDC 连接器实现了 Exactly-Once 的读取语义。它会在 checkpoint 时,把 Debezium 的 offset 也保存到 Flink 的状态中。这样,即使任务重启,也能精确地从上次中断的位置继续读取,不会重复消费,也不会漏掉数据。

不过,这里有个细节要注意:Exactly-Once 保证的是“读取”层面。如果你的下游是 Kafka 或数据库,还需要配合 幂等写入 或 事务写入 才能实现端到端的 Exactly-Once。否则,Flink 保证不丢不重,但下游可能因为网络重试等原因导致重复写入。

我曾经踩过的坑:有一次做 MySQL → Kafka 的 CDC 同步,Flink 端配置了 Exactly-Once,但 Kafka 生产者没开幂等。结果网络抖动导致一批数据重复发送到了 Kafka。后来我同时开启了 Kafka 生产者的 enable.idempotence=true 和 acks=all,才真正做到了端到端不重复。

总结一下,Flink CDC 的 Exactly-Once 语义依赖三个要素:

组件 职责 保证级别
Debezium 记录 binlog offset,支持断点续传 At-Least-Once(配合 Flink 可升级为 Exactly-Once)
Flink Checkpoint 持久化状态和 offset,故障恢复 Exactly-Once(状态层面)
下游写入 幂等或事务写入,避免重复 端到端 Exactly-Once(需额外配置)

嗯,这一章的内容就到这里。Debezium 的内核、Snapshot 与 Streaming 的配合、Changelog Stream 的 RowKind 语义,以及 Exactly-Once 的实现机制,都是理解 Flink CDC 的基石。你把这些搞清楚了,后面再讲具体配置和调优,就会顺畅很多。