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


                                                                                                                                                <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,会导致哪些真实的生产问题。

文章结构如下:

  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 | 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

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

  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,整个行业在逻辑上都是完全一致的。

                                                                                </div>



Source link

未经允许不得转载:紫竹林-程序员中文网 » 为什么实时同步 UPDATE 要两条记录?Apache SeaTunnel 全链路拆解

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
关于我们 免责申明 意见反馈 隐私政策
程序员中文网:公益在线网站,帮助学习者快速成长!
关注微信 技术交流
推荐文章
每天精选资源文章推送
推荐文章
随时随地碎片化学习
推荐文章
发现有趣的