重放去重是什么
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 重放后若被简单跳过或重复应用,数据状态会回退到更早的版本,表现为「改过的又被改回去」。