流式事实表去重
流式链路的重投是常态:消费者重启、检查点回滚、上游补偿发送,都会让同一条消息再来一次。去重要做两层:流内用消息唯一号加事件时间做窗口去重,保证下游在窗口内只看到一条;入库再用主键做 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 给得过长,状态会无限膨胀,作业内存和检查点体积持续上涨。
去重键的选择决定成败:用消息号去重只能挡住重复投递,挡不住业务层面的重放(同一订单被上游重新生成新消息号)。业务唯一键与消息号两个维度通常都要建,落库时以业务键为准做最终幂等。