首页/数据/数仓建模速查/流式事实表的去重与幂等
维度与事实设计149 条

流式事实表的去重与幂等

消息重投是常态:用业务唯一键 + 版本水位做流内去重(ROW_NUMBER)与入库幂等(主键 upsert),下游才能安全重放。

数仓建模速查数据更新于 2026-10-13

流式事实表去重

流式链路的重投是常态:消费者重启、检查点回滚、上游补偿发送,都会让同一条消息再来一次。去重要做两层:流内用消息唯一号加事件时间做窗口去重,保证下游在窗口内只看到一条;入库再用主键做 upsert,保证重放不产生重复行。只做流内去重、入库靠追加,一旦重放数据照样翻倍。

基本信息

流内去重按消息唯一号分区,按事件时间取首条
入库幂等主键 upsert,重复写入覆盖同一行
状态管理去重状态必须设 TTL,窗口有界
典型引擎Flink SQL、Kafka 加 Flink、Spark Structured Streaming
水位处理进度可恢复,重启后从检查点继续

典型使用场景

  • 订单实时入仓:上游为保可靠会重复投递,下游必须能吃掉重复
  • 埋点实时链路:客户端重试与网关重发很常见
  • CDC 同步:全量加增量切换阶段会重复推送同一批记录
  • 作业重启重放:从检查点回溯的那段数据必须幂等消化
  • 多路合并:同一业务事件来自内网主链路与备份链路两条通道

流内去重与状态控制

流内去重与状态控制
-- 按消息唯一号取首条,窗口内去重
SELECT msg_id, order_id, amount, event_time
FROM (
  SELECT *, ROW_NUMBER() OVER (
           PARTITION BY msg_id ORDER BY event_time ASC) AS rn
  FROM kafka_order_source
) WHERE rn = 1;
-- 去重状态设 TTL,与消息保留期对齐
-- SELECT *, ROW_NUMBER() OVER (PARTITION BY msg_id ORDER BY event_time) AS rn
-- FROM kafka_order_source /*+ STATE_TTL('30 d') */;

两层去重对照

层次解决什么失败后果
流内窗口去重窗口期内同键重复窗口外重投仍会重复
入库主键 upsert任意时点重放下游反复读取会数到多行
水位可恢复重启不丢不重重启后从头消费,重复量剧增
去重状态必须与消息保留期对齐:状态 TTL 短于上游可重投的时间跨度,旧消息重投时状态已经过期,去重直接失效。反过来 TTL 给得过长,状态会无限膨胀,作业内存和检查点体积持续上涨。
去重键的选择决定成败:用消息号去重只能挡住重复投递,挡不住业务层面的重放(同一订单被上游重新生成新消息号)。业务唯一键与消息号两个维度通常都要建,落库时以业务键为准做最终幂等。

易错点

  • 去重状态不设 TTL:作业内存持续上涨,几天后检查点超时导致任务失败
  • 只做流内去重、入库不幂等:重放或补数时行数翻倍,指标与离线对不上
  • 去重键只取消息号:上游重新生成消息号的业务重放完全挡不住
  • 水位存在消息体内而非检查点:重启后从头消费,重复量超过窗口处理能力
  • 状态后端与并行度调整后无回归测试:去重结果随并行度变化而漂移

数据来源与参考

  • Apache Flink 官方文档:Deduplication
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌