<p><img src="https://oscimg.oschina.net/oscnet//044334f400912ae0bfd45ae0e6ff62ed.jpg" alt=""></p>
在实时数据平台、实时数仓、数据湖入湖、分布式数据复制这些场景中,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 模式:
当你执行:
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 | score
A | 100
A | 200
执行:
update t set score=300 where name="A";
CDC 如果只发 AFTER:
A, 300
A, 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_BEFORE
UPDATE_AFTER
SeaTunnel 在 MySQL-CDC Source 中会明确输出两个事件:
第一条:UPDATE_BEFORE
第二条:UPDATE_AFTER
这两个事件是严格绑定的,意味着:
- 整个事件在分布式环境中可重放
- 可恢复现场
- 可保持顺序
- 可用于数据湖的合并
下面通过架构图展示 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 在这些系统中本身是一个复合操作:
- 找到旧数据所在的数据文件
- 删除旧数据
- 写入新版本数据文件
示意图如下:
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,整个行业在逻辑上都是完全一致的。
</div>
相关推荐
- AI时代的数据价值兑现:基于Oinone的模型-集成-智能一体化路径
- 从工具到“企业底座”:Oinone 如何重塑低代码的产品化能力
- Apache Doris 实时更新全解:从设计原理到最佳实践|Deep Dive
- 为什么国内许多著名开源项目经常虎头蛇尾?
- 全面对比:Apache SeaTunnel VS. DataX、Flink CDC 和 Talend谁更强?
- GSoC 学生太强了!印度开发者为 DolphinScheduler 做出的 OIDC 升级内幕
- Apache DolphinScheduler VS. Crontab、Airflow:效率对比实测
- 首届 Apache Gluten 社区年度盛会 —— GlutenCon 2025 正式启动!