写点什么

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

作者:白鲸开源
  • 2025-12-04
    天津
  • 本文字数:3288 字

    阅读完需:约 11 分钟


在实时数据平台、实时数仓、数据湖入湖、分布式数据复制这些场景中,CDC(Change Data Capture)几乎成为构建数据链路的标准能力。无论是构建 StarRocks、Doris、ClickHouse 的实时数仓,还是向 Iceberg、Paimon、Hudi 等湖仓写实时变更,亦或是做同库/跨库的实时同步,CDC 都是核心基础能力, 下文以 MySQL CDC 为例。


CDC 中一个常见的问题经常被忽视


为什么 MySQL CDC 的 UPDATE 事件必须输出两条记录,一条 BEFORE,一条 AFTER? 为什么不能只输出最终的新值? 如果仅有 AFTER,难道就不能同步吗?


表面看确实如此,但深入到一致性、幂等性、回放、主键处理、数据湖 Merge、分布式乱序恢复等机制后,就会发现: UPDATE 拆成两条记录不是“形式要求”,而是建立整个 CDC 正确性语义的基石。


本文将从 MySQL Binlog 的内部结构,到 CDC 解析框架(以 SeaTunnel 为主),再到数据湖更新机制,深入分析为什么 UPDATE 必须包含 BEFORE 与 AFTER 两条记录,以及如果缺失 BEFORE,会导致哪些真实的生产问题。


文章结构如下:


  1. MySQL Binlog 中 UPDATE 的真实结构

  2. 为什么 CDC 不可能用一条记录正确表达 UPDATE

  3. 在分布式环境中,CDC 为什么无法只靠 AFTER 保证一致性

  4. SeaTunnel 的 CDC 架构解析

  5. 数据湖与数据仓库 Sink 如何使用 BEFORE

  6. 生产案例分析

  7. 总结


无论你是否从事 CDC、数据平台、实时链路、数据仓库、数据库开发,这篇文章都可以帮助你更系统地理解 CDC 的核心工程语义。

一、MySQL Binlog 中的 UPDATE 不是“一条记录”

很多人以为 MySQL 的 UPDATE 在 binlog 中就是一条记录,这是一个常见误解。


先看 MySQL 的 ROW 模式:


当你执行:


update t set price = 200 where id = 1;
复制代码


Binlog 写入的不是:


id=1, price=200
复制代码


而是这样的结构:


update_rows_event {    before_image: {id:1, price:100}    after_image:  {id:1, price:200}}
复制代码


也就是说,MySQL 内部从一开始就将 UPDATE 视为:


旧值(before)与新值(after) 二元的事件。


为什么 MySQL 要这么做?很简单:


一条 UPDATE 操作,本质上是从旧状态过渡到新状态,这个过渡只有用 before+after 才能完整表达。


否则,你将无法从数据库角度重放、恢复、回滚、校验事务。


换句话说:MySQL Binlog 的结构就决定了 CDC 必须生成两条记录。

二、如果 CDC 只有 AFTER,将会在多个核心场景中无法工作

很多新手工程师直觉是: “只传最新值不就够了吗?”


下面通过六个生产环境里非常典型的例子说明,只有 AFTER 将导致整个链路失效。


  • 场景 1:无法判断是否真的发生更新(数据重复写入、数据湖无效 merge)


如果执行:


update t set price = 200 where id=1;
复制代码


但事务前 price 就是 200。


如果 CDC 只发送 AFTER:


{id=1, price=200}
复制代码


下游根本无法判断:


该 UPDATE 是否为无变化更新 是否需要写入数据湖 是否触发 Merge 操作 是否会导致指标重复计算
复制代码


例如 Iceberg 的 Merge 是一个高成本操作,如果无故触发,直接造成资源浪费。


金融行业的账务、交易、风控数据,非常强调“是否真的更新过”。 因此,必须依赖 BEFORE 才能判断更新是否真实发生。


  • 场景 2:主键更新无法处理(跨库同步将直接失败)


例如:


update user set id=2 where id=1;
复制代码


如果只有 AFTER:


{id=2}
复制代码


下游根本不知道旧主键是 1,无法删除它。这样会导致以下结果:


无法删除 id=1最终会产生两条记录唯一键冲突
复制代码


这是跨库实时同步最常见的灾难场景。 只有 BEFORE 能提供旧主键。


  • 场景 3:无主键表无法定位行(数据将错误更新)


表中存在重复值:


name | scoreA    | 100A    | 200
复制代码


执行:


update t set score=300 where name='A';
复制代码


CDC 如果只发 AFTER:


A, 300A, 300
复制代码


请问:


原本 100 -> 300? 原本 200 -> 300?
复制代码


你根本无法知道。


在没有 BEFORE 的情况下,只依靠主键或唯一键是不可能正确映射的。


  • 场景 4:无法保证 Exactly-Once(幂等性失效)


CDC 系统会因为分布式恢复、网络重试、checkpoint 回放而重复发送事件。


如果只有 AFTER:


你无法判断重复事件与真实变更。


这是流式计算里最根本的幂等性问题。


  • 场景 5:Binlog 多线程导致条目乱序时,无法恢复真实状态


MySQL 多线程更新可能导致解析端接收事件乱序。


示例:


线程1:100 -> 120 线程2:120 -> 200
复制代码


乱序后:


先收到 AFTER=200 再收到 AFTER=120
复制代码


没有 BEFORE,你根本无法判断 120 是否应覆盖 200。


  • 场景 6:数据湖(Paimon、Iceberg、Hudi)必须依赖 BEFORE 做 Delete 操作


数据湖的更新一般都是:


DELETE old_row INSERT new_row
复制代码


例如:


DELETE WHERE id=1 AND price=100  INSERT {id:1, price:200}
复制代码


DELETE 必须引用 BEFORE 中的完整旧值。


因此,数据湖写入本身就要求 CDC 的 UPDATE 输出 before 与 after。

三、SeaTunnel 为什么必须深度支持 BEFORE/AFTER?

SeaTunnel 的 CDC 模型是基于 Debezium 的日志解析能力构建的,其内部采用四种 RowKind:


INSERT DELETE UPDATE_BEFOREUPDATE_AFTER
复制代码


SeaTunnel 在 MySQL-CDC Source 中会明确输出两个事件:


第一条:UPDATE_BEFORE


第二条:UPDATE_AFTER


这两个事件是严格绑定的,意味着:


  1. 整个事件在分布式环境中可重放

  2. 可恢复现场

  3. 可保持顺序

  4. 可用于数据湖的合并


下面通过架构图展示 SeaTunnel 如何处理 UPDATE:


         MySQL Binlog (ROW)                 |          UpdateRowsEvent           before, after                 |    SeaTunnel MySQL-CDC Parser                 |    -------------------------    |                       |  UPDATE_BEFORE      UPDATE_AFTER   old row             new row
复制代码


而后端 Sink 根据 RowKind 决定行为:


UPDATE_BEFORE → 执行 Delete UPDATE_AFTER → 执行 Insert
复制代码


无论是写 OLAP(Doris、StarRocks)、写数据湖(Paimon、Iceberg)、写消息队列(Kafka),这套语义都是一致的。


如果没有 BEFORE,整个链路无法工作。

四、数据湖为何高度依赖 BEFORE?

Iceberg、Paimon、Hudi 等湖仓架构具备 ACID 事务能力,但 UPDATE 在这些系统中本身是一个复合操作:


  1. 找到旧数据所在的数据文件

  2. 删除旧数据

  3. 写入新版本数据文件


示意图如下:


             UPDATE event                   |  -------------------------------------  |                                   |
复制代码


DELETE old_row INSERT new_row 湖仓系统无法通过 AFTER 推断 DELETE 的 key。 这意味着:


缺失 BEFORE → UPDATE 无法正确执行 → 最终数据不一致。


特别是在金融场景中,例如交易流水、资产变动记录、持仓数据,一旦记录不一致,将直接影响指标计算,甚至违反监管要求。

五、生产案例分析

下面举 2 个案例,说明 BEFORE 的重要性。


  • 案例 1:客户表被重复写入,导致同一客户多条记录(主键变更新更失败)


一家游戏公司采用自建 CDC 方案,未采集 BEFORE,只采集 AFTER。


当用户修改手机号时,底层业务更新了复合主键字段,结果目标库产生重复记录。 因为目标库无法知道旧主键值。


最终导致系统中同一用户出现多条记录的数据质量问题。


  • 案例 2:数据湖入湖 Merge 大量失败


一家基金公司的 Iceberg 入湖过程中,只使用 AFTER 构建 Merge 条件。


结果 Delete 无法匹配上旧数据,导致查询结果中存在大量脏数据。


最终必须重建链路,补齐 BEFORE 信息。

六、理论基础:CDC UPDATE 的数学模型

从数据库理论角度看,事务 T 将一条记录从状态 A 变成状态 B,CDC 想要正确表达它,必须输出:


(A, B)


如果只输出 B,CDC 失去了以下能力:


无法判断 T 是否真的发生无法判断 T 的幂等性无法恢复 T 的执行顺序无法用于分布式 Replay无法基于 A 做差异计算无法基于 A 做计算校验
复制代码


换句话说,CDC 把事务转化为了一个“可在下游重建的事件流”, 如果没有 BEFORE,事件流是不完整的。

七、最后总结:UPDATE 两条记录不是设计选择,而是工程必然

为什么 UPDATE 必须是两条?


因为 CDC 想要做到以下能力:


可回放可恢复可重建可乱序容忍可幂等可验证可用于湖仓 Merge可处理主键变更
复制代码


这些能力的前提都是:


UPDATE = BEFORE + AFTER


一条 AFTER 永远无法表达一次完整的更新。


因此,从 MySQL Binlog 的实现,到 Debezium,再到专为数据同步而生的新一代开源工具 Apache SeaTunnel,整个行业在逻辑上都是完全一致的。

发布于: 刚刚阅读数: 2
用户头像

白鲸开源

关注

一家开源原生的DataOps商业公司。 2022-03-18 加入

致力于打造下一代开源原生的DataOps 平台,助力企业在大数据和云时代,智能化地完成多数据源、多云及信创环境的数据集成、调度开发和治理,以提高企业解决数据问题的效率,提升企业分析洞察能力和决策能力。

评论

发布
暂无评论
为什么实时同步 UPDATE 要两条记录?Apache SeaTunnel 全链路拆解_数据库_白鲸开源_InfoQ写作社区