外观
数仓幂等写入怎么落地
难度与前置
难度:进阶
前置:会按分区调度 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};要求:
- 任务只负责自己的分区,不写别人的
dt - 动态分区开启时限制最大分区数,防止写飞
- 成功标记(调度 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. 与分层的关系
常见坑(至少牢记这些)
INSERT INTO当重跑
必双份;重跑必须覆盖或按键合并。OVERWRITE 写错分区
参数dt传错覆盖历史;上线前干跑与权限收紧。成功标记先于数据可见
下游开跑读到旧/空数据;标记必须后置。实时追加 + 批 OVERWRITE 混用无约定
互相踩踏;同一分区写者要串行或分层表。以为事务表就不用设计幂等键
事务保证原子,不自动保证业务键不重复。
如何验收(可量化)
- [ ] 同一作业对同一分区连续执行 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 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。