外观
乱序与重复消费怎么治
难度与前置
难度:进阶
前置:会看实时指标与离线对账差;了解 Watermark / 消费位移。
读完能做什么:把乱序与重复拆开治理——时间语义用 WM/迟到策略,重复用业务键幂等,并用对账与侧输出验收。
适用场景
- 实时指标低于/高于离线,怀疑迟到丢弃或重复多算
- Kafka 重平衡、上游重放、Sink 重试后出现双计
- 多分区、多上游时钟,事件到达顺序不稳定
不适用:
- 纯处理时间看板且业务接受「何时算到何时」(仍建议防重复)
- 离线可全量重跑且无实时 SLO(优先批处理)
- 源端时间字段大量非法(先修质量)
真实场景(一条)
实时订单 GMV:Kafka 两上游(下单服务 + 支付回调)合并。支付消息常比下单晚 10s~3min;消费者 at-least-once。结果:① 窗口过早关闭丢支付成功;② failover 后同一 pay_id 双计。分钟 GMV 相对离线抖动 ±6%。
治理:事件时间 WM=1min + allowedLateness=2min;侧输出统计迟到;状态去重 TTL=24h 按 pay_id;入仓 MERGE。抖动降到 ±0.5% 内,侧输出有告警。
怎么做(步骤清单)
1. 先定性:是乱序问题还是重复问题
| 现象 | 更偏 |
|---|---|
| 实时 < 离线,侧输出/迟到量大 | 乱序 / WM 过紧 |
| 实时 > 离线,主键有双行 | 重复消费 / 非幂等 Sink |
| 两者都有 | 分阶段治:先重复(易抬高),再迟到(易压低) |
sql
-- 离线粗判重复
SELECT pay_id, COUNT(*) AS c
FROM dwd_pay_rt_di
WHERE dt = '${biz_date}'
GROUP BY pay_id
HAVING COUNT(*) > 1
LIMIT 20;
-- 粗判迟到:ingest 相对 event
SELECT approx_percentile(
unix_timestamp(ingest_ts) - unix_timestamp(event_ts),
ARRAY(0.5, 0.9, 0.99)
) AS delay_sec
FROM ods_pay_rt
WHERE dt = '${biz_date}';2. 乱序:事件时间 + Watermark + 迟到策略
详见 Watermark 与迟到。落地最小集:
- 指定事件时间字段与时区
- WM ≈ p99 乱序
allowedLateness覆盖业务可接受补算窗- 超出部分进侧输出 / 死信,供补数
java
WatermarkStrategy
.<PayEvent>forBoundedOutOfOrderness(Duration.ofMinutes(1))
.withTimestampAssigner((e, ts) -> e.getEventTs())
.withIdleness(Duration.ofMinutes(2));3. 重复:业务键幂等(比「只靠 EOS」更稳)
幂等层次:
sql
-- Flink SQL 去重示意
SELECT pay_id, amount, event_ts
FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY pay_id ORDER BY event_ts DESC) AS rn
FROM pay_source
) t
WHERE rn = 1;4. 消费端减少「多余重复」
- 合理
max.poll.interval/ 会话超时,降低不必要 rebalance - 处理与提交位移的语义清晰(先处理成功再提交,或事务提交)
- 上游重放要带 replay_id / epoch,下游可按 epoch 隔离
bash
# 看消费组频繁 rebalance(重复高发信号)
rg -n "Revoking|Assigned|rebalance" consumer.log | tail -505. 双源乱序合并
多上游 Union 时:
- 统一时钟字段与校准
- 空闲分区
idleness,避免一源静默拖死 WM - 业务上用「状态机」:下单→支付成功,允许支付先到则缓冲
6. 对账与补数闭环
- 分钟/小时:实时 vs 离线同口径
- 侧输出量超阈值 → 告警并触发补算作业
- 重复率超阈值 → 查 failover 与 Sink
sql
SELECT
r.dt_hour,
r.gmv AS rt_gmv,
o.gmv AS off_gmv,
(r.gmv - o.gmv) / NULLIF(o.gmv, 0) AS rel_diff
FROM ads_pay_gmv_rt_1h r
JOIN ads_pay_gmv_off_1h o
ON r.dt_hour = o.dt_hour
WHERE r.dt_hour >= '${biz_date}';常见坑(至少牢记这些)
用处理时间窗口「掩盖」乱序
结果好看但不对账。对账口径必须事件时间。只开 EOS 不做业务去重
上游真实双发(两次支付回调)EOS 也去不掉。去重状态无 TTL
重复治好了,状态膨胀把作业拖死。WM 过小 + 无侧输出
数默默变少,业务以为「增长不行」。补数与实时双写未隔离
离线回放与实时重叠区间未去重,短期暴增。
如何验收(可量化)
- [ ] 延迟分布 p99 已测,WM/Lateness 有据
- [ ] 主键重复率 < 约定(如 1e-6)或为 0
- [ ] 实时 vs 离线相对误差在 SLA 带内
- [ ] 侧输出有监控与补数 runbook
- [ ] Kill 消费实例 / TM 演练后指标不越带
sql
-- 验收重复
SELECT
COUNT(*) AS rows,
COUNT(DISTINCT pay_id) AS keys,
COUNT(*) - COUNT(DISTINCT pay_id) AS dup_rows
FROM dwd_pay_di
WHERE dt = '${biz_date}';bash
# 验收侧输出
# late_records_per_min < threshold相关面试题
占位:与题库小程序对齐后补链接(建议关联:乱序、重复消费、Watermark、幂等去重、流批对账)。
改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。