为什么实时同步 UPDATE 要两条记录?Apache SeaTunnel 全链路拆解
在实时数据平台、实时数仓、数据湖入湖、分布式数据复制这些场景中,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,会导致哪些真实的生产问题。
文章结构如下:
MySQL Binlog 中 UPDATE 的真实结构
为什么 CDC 不可能用一条记录正确表达 UPDATE
在分布式环境中,CDC 为什么无法只靠 AFTER 保证一致性
SeaTunnel 的 CDC 架构解析
数据湖与数据仓库 Sink 如何使用 BEFORE
生产案例分析
总结
无论你是否从事 CDC、数据平台、实时链路、数据仓库、数据库开发,这篇文章都可以帮助你更系统地理解 CDC 的核心工程语义。
一、MySQL Binlog 中的 UPDATE 不是“一条记录”
很多人以为 MySQL 的 UPDATE 在 binlog 中就是一条记录,这是一个常见误解。
先看 MySQL 的 ROW 模式:
当你执行:
Binlog 写入的不是:
而是这样的结构:
也就是说,MySQL 内部从一开始就将 UPDATE 视为:
旧值(before)与新值(after) 二元的事件。
为什么 MySQL 要这么做?很简单:
一条 UPDATE 操作,本质上是从旧状态过渡到新状态,这个过渡只有用 before+after 才能完整表达。
否则,你将无法从数据库角度重放、恢复、回滚、校验事务。
换句话说:MySQL Binlog 的结构就决定了 CDC 必须生成两条记录。
二、如果 CDC 只有 AFTER,将会在多个核心场景中无法工作
很多新手工程师直觉是: “只传最新值不就够了吗?”
下面通过六个生产环境里非常典型的例子说明,只有 AFTER 将导致整个链路失效。
场景 1:无法判断是否真的发生更新(数据重复写入、数据湖无效 merge)
如果执行:
但事务前 price 就是 200。
如果 CDC 只发送 AFTER:
下游根本无法判断:
例如 Iceberg 的 Merge 是一个高成本操作,如果无故触发,直接造成资源浪费。
金融行业的账务、交易、风控数据,非常强调“是否真的更新过”。 因此,必须依赖 BEFORE 才能判断更新是否真实发生。
场景 2:主键更新无法处理(跨库同步将直接失败)
例如:
如果只有 AFTER:
下游根本不知道旧主键是 1,无法删除它。这样会导致以下结果:
这是跨库实时同步最常见的灾难场景。 只有 BEFORE 能提供旧主键。
场景 3:无主键表无法定位行(数据将错误更新)
表中存在重复值:
执行:
CDC 如果只发 AFTER:
请问:
你根本无法知道。
在没有 BEFORE 的情况下,只依靠主键或唯一键是不可能正确映射的。
场景 4:无法保证 Exactly-Once(幂等性失效)
CDC 系统会因为分布式恢复、网络重试、checkpoint 回放而重复发送事件。
如果只有 AFTER:
你无法判断重复事件与真实变更。
这是流式计算里最根本的幂等性问题。
场景 5:Binlog 多线程导致条目乱序时,无法恢复真实状态
MySQL 多线程更新可能导致解析端接收事件乱序。
示例:
乱序后:
没有 BEFORE,你根本无法判断 120 是否应覆盖 200。
场景 6:数据湖(Paimon、Iceberg、Hudi)必须依赖 BEFORE 做 Delete 操作
数据湖的更新一般都是:
例如:
DELETE 必须引用 BEFORE 中的完整旧值。
因此,数据湖写入本身就要求 CDC 的 UPDATE 输出 before 与 after。
三、SeaTunnel 为什么必须深度支持 BEFORE/AFTER?
SeaTunnel 的 CDC 模型是基于 Debezium 的日志解析能力构建的,其内部采用四种 RowKind:
SeaTunnel 在 MySQL-CDC Source 中会明确输出两个事件:
第一条:UPDATE_BEFORE
第二条:UPDATE_AFTER
这两个事件是严格绑定的,意味着:
整个事件在分布式环境中可重放
可恢复现场
可保持顺序
可用于数据湖的合并
下面通过架构图展示 SeaTunnel 如何处理 UPDATE:
而后端 Sink 根据 RowKind 决定行为:
无论是写 OLAP(Doris、StarRocks)、写数据湖(Paimon、Iceberg)、写消息队列(Kafka),这套语义都是一致的。
如果没有 BEFORE,整个链路无法工作。
四、数据湖为何高度依赖 BEFORE?
Iceberg、Paimon、Hudi 等湖仓架构具备 ACID 事务能力,但 UPDATE 在这些系统中本身是一个复合操作:
找到旧数据所在的数据文件
删除旧数据
写入新版本数据文件
示意图如下:
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 失去了以下能力:
换句话说,CDC 把事务转化为了一个“可在下游重建的事件流”, 如果没有 BEFORE,事件流是不完整的。
七、最后总结:UPDATE 两条记录不是设计选择,而是工程必然
为什么 UPDATE 必须是两条?
因为 CDC 想要做到以下能力:
这些能力的前提都是:
UPDATE = BEFORE + AFTER
一条 AFTER 永远无法表达一次完整的更新。
因此,从 MySQL Binlog 的实现,到 Debezium,再到专为数据同步而生的新一代开源工具 Apache SeaTunnel,整个行业在逻辑上都是完全一致的。
版权声明: 本文为 InfoQ 作者【白鲸开源】的原创文章。
原文链接:【http://xie.infoq.cn/article/7ae29361f023f957a83d4e01a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。







评论