为什么实时 UPDATE 同步需要两条记录?Apache SeaTunnel 全链路拆解
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 – 缺失主键(行歧义)
| 名称 | 分数 |
|---|---|
| A | 100 |
| A | 200 |
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_BEFORE 与 UPDATE_AFTER 事件,以保证可重放、可恢复,并兼容下游系统。
理解并保留 BEFORE 镜像对于在数据库、数据仓库和数据湖之间构建可靠、一致的实时管道至关重要。