为什么实时 UPDATE 同步需要两条记录?Apache SeaTunnel 全链路拆解

发布: (2025年12月4日 GMT+8 16:03)
7 min read
原文: Dev.to

Source: Dev.to

介绍

在实时数据平台——实时仓库、数据湖摄取以及分布式数据复制——中,CDC(变更数据捕获)已经成为构建现代管道的标准能力。无论你是将数据加载到 StarRocks、Doris、ClickHouse、Iceberg、Paimon 或 Hudi,还是在数据库之间同步,CDC 都是核心基础。

一个常被忽视的 CDC 问题是:

  • 为什么 MySQL CDC 的 UPDATE 事件必须输出两条记录——一条 BEFORE 和一条 AFTER?
  • 为什么不能只输出最终的新值?
  • 如果我们只有 AFTER,系统不能仍然正确同步吗?

乍看之下似乎可以,但一致性、幂等性、可重放性、主键处理、数据湖合并语义以及乱序恢复都表明,将 UPDATE 拆分为 BEFORE + AFTER 并不是“格式选择”——它是 CDC 正确性的根本要求。

MySQL Binlog 中 UPDATE 的真实结构

MySQL 并不会把 UPDATE 写成单条记录。在基于行的 binlog 格式中,事件看起来是:

update_rows_event {
    before_image: {id: 1, price: 100}
    after_image:  {id: 1, price: 200}
}

当你执行:

UPDATE t SET price = 200 WHERE id = 1;

binlog 会同时记录旧值(BEFORE)和新值(AFTER)。这对描述状态转变是完整的,能够支持正确的回放、回滚以及事务验证。

为什么 CDC 不能仅用一条记录表示 UPDATE

场景 1 – 检测真实变更

UPDATE t SET price = 200 WHERE id = 1;

如果原来的 price 已经是 200,仅 AFTER 的事件 ({id:1, price:200}) 会让下游系统无法判断数据是否真的发生了变化。这会导致:

  • 对数据湖的无意义写入(昂贵的 Iceberg 合并)
  • 指标重新计算错误
  • 计算资源浪费

场景 2 – 主键更新

UPDATE user SET id = 2 WHERE id = 1;

仅 AFTER 的事件 ({id:2}) 缺少旧的主键值,下游系统无法删除原来的行。后果包括:

  • 记录重复
  • 唯一键冲突
  • 跨库复制破裂

场景 3 – 缺失主键(行歧义)

名称分数
A100
A200
UPDATE t SET score = 300 WHERE name = 'A';

仅 AFTER 会产生两条相同的行 (A, 300)。没有 BEFORE,系统无法确定每条更新对应原来的哪一行。

场景 4 – Exactly‑Once 保证(幂等性)

CDC 管道经常因以下原因重新发送事件:

  • 分布式恢复
  • 网络重试
  • 检查点回放
  • 消费者重启

仅 AFTER 的事件无法与重复事件区分,破坏幂等性保证。

场景 5 – Binlog 事件乱序

MySQL 多线程复制可能产生:

  • 线程 1:100 → 120
  • 线程 2:120 → 200

如果 AFTER = 200 先于 AFTER = 120 到达,系统无法知道 120 应该覆盖 200,因为缺少 BEFORE 镜像。

场景 6 – 数据湖删除需求

数据湖的更新语义通常执行:

DELETE old_row
INSERT new_row

DELETE 必须匹配精确的 BEFORE 镜像(例如 WHERE id=1 AND price=100)。缺少 BEFORE 会导致 DELETE 不可执行,进而产生不一致的数据——在受监管的金融环境中尤为致命。

SeaTunnel 的 CDC 架构

SeaTunnel 基于 Debezium 的日志解析,并定义了四种 RowKind

  • INSERT(插入)
  • DELETE(删除)
  • UPDATE_BEFORE(更新前)
  • UPDATE_AFTER(更新后)

因此 MySQL‑CDC 源会发出两条紧密耦合的事件:

UPDATE_BEFORE → 旧行(DELETE)
UPDATE_AFTER  → 新行(INSERT)

处理流程

MySQL Binlog (ROW)
    |
UpdateRowsEvent (before, after)
    |
SeaTunnel MySQL‑CDC Parser
    |-------------------|
UPDATE_BEFORE      UPDATE_AFTER
(old row)          (new row)

下游 sink 根据 RowKind 决定操作。该模型确保:

  • 分布式环境下的可重放性
  • 可恢复性
  • 顺序保持
  • 与数据湖合并语义的兼容性

相同的模式同样适用于 OLAP 数据库(Doris、StarRocks)、数据湖(Iceberg、Paimon、Hudi)以及消息系统(Kafka)。省略 BEFORE 将导致整个管道失效。

数据湖和仓库如何消费 BEFORE

Iceberg、Paimon、Hudi 支持 ACID 语义,但一次 UPDATE 是复合操作:

UPDATE event
    |
------------------------------
|                            |
DELETE old_row          INSERT new_row

DELETE 步骤必须匹配精确的 BEFORE 镜像;否则 UPDATE 无法执行,导致数据不一致。

真实生产案例

案例 1 – 客户记录重复(主键更新)

一家游戏公司只捕获 AFTER 事件。当用户更新复合主键(手机号)时,下游系统因为无法删除旧键而产生了重复的客户记录。

案例 2 – Iceberg 合并失败

一家金融机构使用仅 AFTER 的 CDC 将数据写入 Iceberg。DELETE 操作无法匹配旧记录,导致大量不一致数据。后续重建管道,加入 BEFORE 镜像,恢复了正确性。

最终要点

  • UPDATE 是从旧状态到新状态的转变,两个镜像都是实现准确 CDC 所必需的。
  • BEFORE + AFTER 能够实现:
    • 检测真实变更
    • 正确处理主键更新
    • 消除行歧义
    • Exactly‑once 处理(幂等性)
    • 乱序恢复
    • 数据湖中正确的删除/插入语义

SeaTunnel(以及类似的 CDC 框架)会发出配对的 UPDATE_BEFOREUPDATE_AFTER 事件,以保证可重放、可恢复,并兼容下游系统。

理解并保留 BEFORE 镜像对于在数据库、数据仓库和数据湖之间构建可靠、一致的实时管道至关重要。

Back to Blog

相关文章

阅读更多 »

🌑 进入黑暗:Soulbound Codex

演示图片 https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2...