Skip to content

数仓幂等写入怎么落地 ​

难度与前置

难度:进阶
前置:会按分区调度 Hive/Spark 作业;知道重跑可能导致数据重复。
读完能做什么:为离线分区表与近实时表选定幂等策略,写出可重跑作业,并用重复执行对账验收。

适用场景 ​

  • 调度失败重试、上游补数、手动重跑同一 dt
  • 实时作业 failover 重放导致重复写
  • 多任务可能写同一张目标表

不适用:

  • 纯追加且允许重复、下游永远做去重的临时探索表(仍建议标清楚)
  • 无主键又无法分区覆盖的无设计表(先补模型)

真实场景(一条) ​

DWD 消耗日表用 INSERT INTO 追加。某日任务跑到一半失败,调度重试后又跑成功,分区内金额 翻倍。对账第二天发现,运营日报已错误放出。

改法:改为 INSERT OVERWRITE TABLE ... PARTITION (dt=...);质量规则加分区行数/金额波动告警;重跑演练纳入发布检查。同一分区连续写 3 次,结果哈希一致。

怎么做(步骤清单) ​

1. 先选幂等粒度 ​

粒度典型手段适合
分区级INSERT OVERWRITE 整分区离线日批、小时批
主键级MERGE / Upsert / DELETE+INSERT拉链、镜像、CDC
文件级事务Iceberg/Hudi/Delta 事务提交湖仓可回滚
下游去重主键约束、幂等键写外部 DB

TIP

幂等的目标:同一输入重复执行 N 次,对外可见结果与成功一次相同。先写清「可见结果」是整分区快照还是主键终态。

2. 离线分区表:优先 OVERWRITE ​

sql
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di
PARTITION (dt = '${biz_date}')
SELECT
  bill_id,
  channel_id,
  play_id,
  cost_amount,
  event_ts,
  is_valid,
  filter_reason,
  current_timestamp() AS etl_ts
FROM tmp_dwd_ad_spend_${biz_date};

要求:

  1. 任务只负责自己的分区,不写别人的 dt
  2. 动态分区开启时限制最大分区数,防止写飞
  3. 成功标记(调度 done 文件 / 元数据表)在 OVERWRITE 成功后 再落

3. 主键 Upsert / MERGE+INSERT ​

sql
-- 示意:先删后插同一业务键集合(小范围补数)
DELETE FROM dws_ad_channel_play_1d
WHERE dt = '${biz_date}' AND channel_id IN (SELECT channel_id FROM tmp_fix_channels);

INSERT INTO dws_ad_channel_play_1d PARTITION (dt = '${biz_date}')
SELECT * FROM tmp_rebuild_channels;

或引擎支持时:

sql
MERGE INTO dws_ad_channel_play_1d t
USING tmp_rebuild s
ON t.dt = s.dt AND t.channel_id = s.channel_id AND t.play_id = s.play_id
WHEN MATCHED THEN UPDATE SET cost_amount = s.cost_amount, bill_cnt = s.bill_cnt
WHEN NOT MATCHED THEN INSERT *;

4. 实时 / 微批写出 ​

手段说明
目标主键 upsert窗口结果多次发射可覆盖
幂等键(业务键 + 窗口)下游唯一约束拒重或覆盖
两阶段提交 Sink与 Checkpoint 对齐,见 Flink 实践
先写暂存再发布目录/分区原子切换

禁止:无键追加 + failover 重放,还宣称 Exactly-Once。

5. 调度与补数约定 ​

text
重跑某 dt:
  1) 上游分区就绪
  2) 本层 OVERWRITE / MERGE 该 dt
  3) 触发下游同 dt 级联
  4) 跑质量规则与对账
  • 禁止「补数用 INSERT INTO 追加到已有分区」除非表语义就是变更日志
  • 跨天迟到数据:写入事件所属 dt 并重跑该 dt,而不是塞进今天分区冒充

6. 质量规则护航 ​

  • 分区行数、金额相对昨日波动阈值
  • 主键唯一(COUNT(*) = COUNT(DISTINCT pk))
  • 重跑前后 checksum 对比
sql
-- 主键唯一
SELECT
  COUNT(*) AS cnt,
  COUNT(DISTINCT bill_id) AS distinct_cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';

-- 重复执行后 checksum(示意)
SELECT SUM(HASH(bill_id, cost_amount)) AS sig, SUM(cost_amount) AS cost_sum
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';

7. 与分层的关系 ​

  • ODS 追加日志表:可保留多版本,但镜像层必须幂等
  • DWD/DWS/ADS 日表:默认分区覆盖
  • 详见 数仓分层、CDC 入仓

常见坑(至少牢记这些) ​

  1. INSERT INTO 当重跑
    必双份;重跑必须覆盖或按键合并。

  2. OVERWRITE 写错分区
    参数 dt 传错覆盖历史;上线前干跑与权限收紧。

  3. 成功标记先于数据可见
    下游开跑读到旧/空数据;标记必须后置。

  4. 实时追加 + 批 OVERWRITE 混用无约定
    互相踩踏;同一分区写者要串行或分层表。

  5. 以为事务表就不用设计幂等键
    事务保证原子,不自动保证业务键不重复。

如何验收(可量化) ​

  • [ ] 同一作业对同一分区连续执行 2~3 次,行数、金额、checksum 不变
  • [ ] 模拟中途失败再重试,最终结果与一次成功一致
  • [ ] 主键唯一规则日更通过
  • [ ] 补数 runbook:指定 dt 级联重跑可操作
  • [ ] 实时链路 failover 演练后无双计(对账 diff≈0)
bash
# 演练伪流程
run_job --dt ${biz_date}
run_job --dt ${biz_date}   # 再跑一次
diff_checksum --table dwd_ad_spend_detail_di --dt ${biz_date}
sql
SELECT SUM(cost_amount) AS cost_sum, COUNT(*) AS cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';

相关面试题 ​

占位:与题库小程序对齐后补链接(建议关联:幂等写入、OVERWRITE vs INTO、MERGE、调度重跑)。


改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。