Skip to content

幂等回刷与重跑怎么设计 ​

难度与前置

难度:中级
前置:已读 数仓分层 与 分区策略;调度能按 dt 重跑任务。
读完能做什么:给一条 ODS→ADS 链路定「同一分区重跑不叠数」的写入契约,能安全回刷并验收。

适用场景 ​

  • 源数据迟到 / 修数,需要对历史 dt 补数或回刷
  • 任务失败重试、手动重跑,必须保证跑几次结果一样
  • 下游已依赖分区表,回刷后要能级联重跑且对账可解释

不适用:

  • 无分区的全量覆盖小维表(可整表替换,但仍需版本或备份策略)
  • 实时 Exactly-Once 语义(本篇按离线分区批处理)
  • 「只追加不更新」且业务允许重复行的流水归档(需另定去重层)

真实场景(一条) ​

投放渠道文件常迟到 T+2。某周一要把 dt=2025-01-08~01-10 三天 ODS 重拉,并级联 DWD/DWS/对账 ADS。

若写入不是幂等的,会出现:

  1. 重跑三次 ODS,分区内行数 ×3,消耗翻倍
  2. 只重跑了 DWD,DWS 仍是旧数,日报与对账打架
  3. 动态分区一次写出多余空目录,元数据膨胀

目标契约:

  • 同一 dt 覆盖写(或先删后写),重复跑结果稳定
  • 回刷列出分区范围,上游完成再下游
  • 回刷前后有行数 / 金额 / 主键唯一验收

怎么做(步骤清单) ​

1. 先写死「幂等」定义(团队共识) ​

对本域日批表,幂等至少包含:

条款含义
同分区可重入任意次重跑 dt=D,结果集与成功跑一次一致
不叠数禁止在原有文件上无控制追加导致重复
可级联上游 dt=D 变更 ⇒ 下游同 dt(或约定窗口)必须重跑
可审计留下回刷人、时间、分区列表、前后校验

TIP

幂等是写入契约,不是调度点一下重跑就自动具备。表格式、插入语句、依赖边都要配合。

2. 选定写入模式(按层推荐) ​

层推荐写法说明
ODS / DWD / DWS 日增量INSERT OVERWRITE TABLE ... PARTITION (dt=...)同分区覆盖;最常见
需要保留旧版本写入新分区或版本目录,再切视图回滚友好,成本更高
引擎支持事务/ACID按官方语义做动态分区覆盖先确认隔离级别与小文件
ADS 全量快照按快照日覆盖,或整表替换+备份避免与明细分区语义混用
sql
-- 推荐:静态分区覆盖(幂等)
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di
PARTITION (dt = '${biz_date}')
SELECT
  bill_id,
  channel_id,
  play_id,
  creative_id,
  cost_amount,
  event_ts,
  account_tz,
  src_system,
  is_valid,
  filter_reason,
  current_timestamp() AS etl_ts
FROM tmp_dwd_ad_spend_${biz_date_nodash};

-- 危险:对已有分区无条件 INSERT INTO 追加(重跑即叠数)
-- INSERT INTO TABLE dwd_ad_spend_detail_di PARTITION (dt = '${biz_date}') SELECT ...

3. 回刷前清单(不满足就先别跑) ​

  • [ ] 明确分区列表:dt IN (...) 或起止日期
  • [ ] 源数据已就绪(文件到达、水位、对账票)
  • [ ] 写入方式是 OVERWRITE / 先删分区再写,而不是盲追加
  • [ ] 下游依赖列表已导出(DWD→DWS→ADS)
  • [ ] 回刷窗口与业务沟通:看板会短暂旧数或雪花
  • [ ] 大范围回刷评估:队列、小文件、元数据压力

4. 级联顺序与调度落点 ​

text
对每个 dt in 回刷列表(升序或按依赖允许的并行度):
  1) ODS  覆盖写 dt
  2) DQ P0 出门禁(主键/空值/空分区)
  3) DWD  覆盖写 dt(可关联当时维表口径,见下)
  4) DQ P0
  5) DWS  覆盖写 dt
  6) ADS  覆盖写 dt / 对账结果
  7) 分区级验收 SQL

维表注意:

  • 回刷历史事实时,维表应用业务当日的 SCD 区间(见 拉链表),不要误用「今天的维」解释昨天的事实
  • 维表自身若也修数,先回刷维,再回刷事实链路

5. 「先删后写」与路径级清理(可选加固) ​

当表是外部表或曾用 INSERT INTO 脏写过:

bash
# 示意:删除目标分区路径后再写(需与 Metastore 分区一致)
# 1) 备份(可选)
hdfs dfs -cp /warehouse/dwd/dwd_ad_spend_detail_di/dt=${biz_date} \
             /backup/dwd_ad_spend_detail_di/dt=${biz_date}_${ts}

# 2) 丢分区 + 删数据(按你们规范二选一或组合)
hive -e "ALTER TABLE dwd_ad_spend_detail_di DROP IF EXISTS PARTITION (dt='${biz_date}');"
hdfs dfs -rm -r -skipTrash /warehouse/dwd/dwd_ad_spend_detail_di/dt=${biz_date}

# 3) INSERT OVERWRITE 或 LOAD 后 MSCK/ADD PARTITION

WARNING

只删 HDFS 不更新 Metastore,或只丢分区不删文件,都会留下「幽灵数据 / 幽灵分区」。删改必须成对。

6. 任务代码层:去掉隐藏的非幂等 ​

检查这些反模式:

反模式后果改法
INSERT INTO 分区叠数改 OVERWRITE
用 etl_ts=now() 当主键一部分每次重跑键变主键用业务键
先读目标表再 merge 却漏删旧行残留按分区全量替换或明确 delete+insert
依赖「第一次全量、以后增量」无状态表重跑丢上下文状态落到表,或分区纯函数重算
回刷跳过 DQ脏数灌下游回刷同样走 P0 出门禁
sql
-- 幂等去重示意:ODS 可能重复推送时,DWD 按业务主键取最新
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di
PARTITION (dt = '${biz_date}')
SELECT
  bill_id, channel_id, play_id, creative_id,
  cost_amount, event_ts, account_tz, src_system,
  is_valid, filter_reason, etl_ts
FROM (
  SELECT
    *,
    ROW_NUMBER() OVER (PARTITION BY bill_id ORDER BY event_ts DESC, etl_ts DESC) AS rn
  FROM ods_ad_spend_di
  WHERE dt = '${biz_date}'
) t
WHERE rn = 1;

7. 回刷执行记录(最小字段) ​

sql
CREATE TABLE IF NOT EXISTS ops_backfill_log (
  backfill_id   STRING,
  table_name    STRING,
  dt_from       STRING,
  dt_to         STRING,
  operator      STRING,
  start_ts      TIMESTAMP,
  end_ts        TIMESTAMP,
  status        STRING COMMENT 'RUNNING/SUCCESS/FAILED',
  row_cnt_before BIGINT,
  row_cnt_after  BIGINT,
  cost_before   DECIMAL(18,4),
  cost_after    DECIMAL(18,4),
  note          STRING
)
COMMENT '分区回刷审计(示意)';

常见坑 ​

  1. INSERT INTO 当重跑
    第二次起必然叠数。日批分区默认 OVERWRITE。

  2. 只重跑中间层
    ODS 变了只跑 DWD,DWS/ADS 仍旧,对账「修好了又坏」。级联清单要全。

  3. 大范围动态分区一次回刷
    写出海量空分区/小文件。按 dt 串行或小批量,并合并输出。

  4. 用当前维表解释历史事实
    渠道改名/改结算主体后,历史消耗口径被改写。回刷要对齐事件日维表。

  5. 外部表漏维护分区元数据
    数据删了分区还在,或反之。DROP PARTITION 与路径删除成对,写后校验 SHOW PARTITIONS。

  6. 回刷跳过质量出门禁
    「赶时间先出数」把重复主键灌进 DWS。回刷与日常共用 P0 规则(见 质量闭环)。

如何验收(可量化) ​

  • [ ] 目标表写入语句为分区 OVERWRITE(或等价先删后写),代码评审可核对
  • [ ] 故意连续重跑两次同 dt:行数、金额、主键集合一致
  • [ ] 回刷级联列表完整;下游同分区更新时间 ≥ 上游
  • [ ] P0:主键唯一、核心度量非空、非空分区
  • [ ] 对账 ADS 与源差异可解释;ops_backfill_log 有成功记录
sql
-- 验收 1:同分区连跑两次的快照对比(将 _run1 / _run2 换成两次导出)
SELECT
  'row_cnt' AS metric,
  r1.cnt AS run1,
  r2.cnt AS run2,
  r1.cnt - r2.cnt AS diff
FROM (SELECT COUNT(*) AS cnt FROM dwd_ad_spend_detail_di WHERE dt = '${biz_date}') r1
CROSS JOIN (SELECT COUNT(*) AS cnt FROM dwd_ad_spend_detail_di_run2 WHERE dt = '${biz_date}') r2
UNION ALL
SELECT
  'cost_sum',
  r1.s, r2.s, r1.s - r2.s
FROM (
  SELECT SUM(cost_amount) AS s FROM dwd_ad_spend_detail_di
  WHERE dt = '${biz_date}' AND is_valid = true
) r1
CROSS JOIN (
  SELECT SUM(cost_amount) AS s FROM dwd_ad_spend_detail_di_run2
  WHERE dt = '${biz_date}' AND is_valid = true
) r2;
-- 期望:diff = 0

-- 验收 2:主键唯一
SELECT bill_id, COUNT(*) AS cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
GROUP BY bill_id
HAVING COUNT(*) > 1
LIMIT 20;
-- 期望:0 行
bash
# 验收 3:连续提交两次同一分区任务(示意)
# airbox/dolphinscheduler/crontab:对 dt=2025-01-10 触发两次 dwd 任务
# 比对两次完成后的 count / sum;并确认下游 dws 也被触发且完成时间更晚

相关面试题 ​

占位:数仓任务如何保证幂等;补数/回刷怎么做;INSERT OVERWRITE 与 INTO 的区别。


改写说明:写入与回刷契约参考 Wood 原创数仓/面试实践,已改写成可执行清单;未包含未公开公司数据与内部调度系统名。