首页/数据/数据工程与 ETL 速查/重放去重:至少一次语义兜底
CDC 与同步53 条

重放去重:至少一次语义兜底

CDC 链路多为至少一次投递:作业重放、位点回退、消费者重平衡都会重复送同一变更。去重可在目标端(主键 upsert 最省事)、加工层(按主键与位点去重,状态存最近窗口)或消息层(Kafka 事务/幂等生产者,只覆盖链路段)实现。

重放去重是什么

CDC 链路绝大多数是至少一次(at-least-once)投递:作业重启、位点回退、消费组重平衡都会把同一条变更再送一遍。这本身不是缺陷,前提是下游能重复消费而不出错。去重可以放在四处:目标端(主键 upsert 最省事)、加工层(按主键与位点去重,状态只保留最近窗口)、消息层(幂等或事务生产者,只覆盖 Kafka 这一段的重复)、调度层(把回补设计成可重复执行)。

基本信息

语义前提至少一次投递,重复是常态,不能靠链路保证恰好一次
目标端去重主键 upsert 或 MERGE,依赖目标表有主键约束
加工层去重按 主键 + 位点(文件与偏移,或 GTID)去重,状态设窗口上限
消息层去重幂等生产者或事务,仅覆盖 Kafka 写入段
调度层回补任务写成可分次、可重复执行
状态管理去重窗口要有界,或落库保存而不是长期驻留内存

典型使用场景

  • 作业重启:位点回退一段重新消费,重复事件需要被吸收
  • 消费组重平衡:分区重新分配期间消息可能被重复处理
  • 回补重放:人为重放某段时间的变更用于修正数据
  • 多路汇聚:同一条变更从备份链路再次到达,需要幂等处理
  • 故障演练:主动制造重复投递,验证下游是否幂等

按位点去重的 SQL 思路

按位点去重的 SQL 思路
-- 同一主键在同一个 binlog 位点上的事件只保留一条
SELECT pk, file_name, pos, op, amount, ts_ms
FROM (
  SELECT pk, file_name, pos, op, amount, ts_ms,
         ROW_NUMBER() OVER (PARTITION BY pk, file_name, pos ORDER BY ts_ms DESC) AS rn
  FROM stage_cdc_events
) t
WHERE t.rn = 1;

目标端幂等写入

目标端幂等写入
-- upsert:重复事件重放结果一致,最省事的去重方式
INSERT INTO dw.orders (order_id, amount, status, updated_at)
VALUES (10086, 199.00, 'PAID', '2026-10-11 10:00:00')
ON DUPLICATE KEY UPDATE
  amount = VALUES(amount), status = VALUES(status), updated_at = VALUES(updated_at);

-- 或 MERGE:按主键匹配后更新,未匹配则插入
MERGE INTO dw.orders AS t
USING stage_cdc_events AS s ON t.order_id = s.order_id
WHEN MATCHED AND s.op IN ('u', 'c') THEN UPDATE SET t.amount = s.amount, t.status = s.status
WHEN NOT MATCHED AND s.op <> 'd' THEN INSERT (order_id, amount, status) VALUES (s.order_id, s.amount, s.status);

去重位置对照

层面做法代价
目标端主键 upsert 或 MERGE目标表需要主键约束,频繁写入小文件增多
加工层按 主键 + 位点 去重,保留最近窗口状态内存受规模限制,窗口要设上限
消息层Kafka 幂等或事务生产者只覆盖 Kafka 消费段,跨系统重复仍存在
调度层回补任务设计为可重复执行全量重算成本较高
去重窗口必须设上限:无界状态会让长跑作业内存持续增长,跑几周后触发内存告警甚至作业崩溃。
只对 insert 去重是不够的:update 与 delete 重放后若被简单跳过或重复应用,数据状态会回退到更早的版本,表现为「改过的又被改回去」。

易错点

  • 只对 insert 去重,update 与 delete 重放后数据状态回退
  • 去重状态无限增长,作业运行数周后内存告警
  • 依赖链路保证恰好一次,未在目标端做幂等,一次重启就产生重复
  • 去重键只用主键,同一主键在不同位点的合法多次变更被错误合并成一次
  • 回补任务不幂等,重跑一次数据翻倍,后续对账全部失真

数据来源与参考

  • Kafka 官方文档(Delivery semantics)
  • Debezium 官方文档(至少一次投递说明)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌