3. 数据重复的根源

在CDC(Change Data Capture)治理中,重复数据几乎不是“偶发异常”,而是分布式链路在故障与恢复之间必然要面对的系统性现象。很多团队第一次遇到重复时,直觉是“下游去重没做好”,但真正的原因往往藏在采集、传输、消费、位点管理和并行处理等多个环节。理解重复的根源,是设计幂等与去重策略的前提。

核心判断:CDC链路中的重复,绝大多数不是“数据被写了两遍”,而是“同一条变更被投递、消费或重放多次”。治理重复,首先要区分重复发生在哪一层、由什么机制触发。

一、核心原理:重复的本质是“至少一次”语义的代价

CDC链路通常由源端日志解析、消息传输、消费写入三段组成。为了保证不丢数据,绝大多数实现选择“至少一次”(at-least-once)投递语义:只要不能确认下游已成功处理,就允许重发。重复因此不是bug,而是可靠性设计的副产品。真正危险的是把“至少一次”误当成“恰好一次”,从而在业务侧缺少幂等保护。

从数据形态看,重复可分为三类:完全相同的记录重复、同一主键的多次变更重复、以及因位点回退导致的历史变更重放。三者的治理难度依次上升,因为后者可能改变最终状态。

CDC链路重复产生的五类根源 源端重试 事务重放/日志重读 网络抖动 ACK丢失/超时重发 位点回退 offset重置/提交失败 多分区并行 乱序/重复消费 故障恢复 重放/回滚 重复变更进入下游 同一条变更被投递/消费/重放多次 治理方向 幂等写入 · 唯一键约束 · 位点精确管理 分区有序 · 故障恢复校验 · 去重窗口

二、五类常见原因逐层拆解

1. 源端重试。源库在事务提交、日志切换或连接中断时,采集组件可能重新读取同一段binlog或WAL。例如MySQL主从切换后,新主库的binlog位置与旧主库不一致,采集器若按旧位点重新拉取,就会把已发送的变更再发一次。这类重复通常表现为“同一事务ID或同一LSN区间重复出现”。

2. 网络抖动。生产者发送消息后未收到Broker的ACK,会触发重发;但Broker可能已经写入成功。Kafka、Pulsar、RocketMQ在开启重试时都可能产生这种“ACK丢失型重复”。其特点是重复记录时间戳接近、内容完全一致。

3. 消费位点回退。消费者处理完数据但位点提交失败,或人为重置offset到较早位置,会导致一批历史变更被重新消费。这是最容易被忽视、也最容易造成状态错乱的一类重复。位点回退不仅产生重复,还可能让旧值覆盖新值。

4. 多分区并行。同一业务主键若被分散到不同分区,多个消费者并行处理时无法保证全局顺序,可能出现“先到的后写、后到的先写”,在重试场景下进一步放大重复。分区键设计不当是根因之一。

5. 故障恢复。任务重启、扩缩容、Checkpoint恢复时,系统会从最近一次成功位点重新拉起。若Checkpoint与位点提交不是原子操作,就会出现“已处理但未记录”的区间被重放。

重复原因 典型触发条件 重复特征 优先治理手段
源端重试 主从切换、日志重读 同事务/LSN区间重复 源端位点校验、事务去重
网络抖动 ACK丢失、发送超时 内容完全一致、时间接近 幂等生产者、消息ID去重
位点回退 提交失败、手动重置 历史变更重放 位点原子提交、回退告警
多分区并行 分区键分散、并发消费 乱序+重复交织 按主键分区、顺序消费
故障恢复 重启、扩缩容、Checkpoint 区间性重放 精确一次Checkpoint、恢复校验

三、案例场景:一次位点回退引发的“数据倒灌”

某电商订单CDC链路中,消费者在处理“订单支付成功”事件后写入数仓,但位点提交因网络超时失败。任务重启后从旧位点恢复,重新消费了最近15分钟的变更。结果数仓中同一订单出现多条支付记录,且部分“已取消”状态被旧的“待支付”覆盖。排查日志可见:

2024-06-11 10:23:41 INFO  Consumer offset commit failed, will retry
2024-06-11 10:23:46 WARN  Commit retry exhausted, offset not advanced
2024-06-11 10:24:02 INFO  Consumer restarted, seek to offset 88231
2024-06-11 10:24:03 INFO  Re-consuming records from offset 88231 to 88990

复盘结论:位点提交与业务写入不在同一事务中,导致“写了但没记位点”。治理方案是引入幂等写入,以订单ID+变更版本号作为唯一键,并在恢复时校验位点与业务状态的一致性。

实践建议:不要试图在链路层彻底消灭重复,而应把重复当作常态输入,在写入层用唯一键、版本号或状态机保证幂等。链路层负责“少重复”,业务层负责“重复无害”。

四、误区与复盘

  • 误区一:认为开启“精确一次”就万事大吉。精确一次通常只在特定组件内部成立,跨系统端到端仍可能重复。
  • 误区二:只在下游做distinct去重。distinct无法处理“同一主键不同版本”的重复,可能保留错误版本。
  • 误区三:忽视位点提交的原子性。位点与业务写入分离,是故障恢复重复的最大来源。
  • 误区四:分区键随意选择。同一主键跨分区会同时引入乱序与重复,治理成本成倍上升。
排障顺序建议:先确认重复发生在源端、传输端还是消费端;再检查位点提交日志与Checkpoint记录;最后核对分区键与消费并发度。不要一上来就写去重SQL,否则容易掩盖真正的链路问题。

总结来看,CDC链路中的重复由源端重试、网络抖动、消费位点回退、多分区并行与故障恢复共同构成。它们不是孤立故障,而是“至少一次”语义、分布式状态管理与并发处理共同作用的结果。只有先定位重复的层次与触发机制,才能设计出既保不丢、又保不重的治理方案。