首页/数据/数据工程与 ETL 速查/投递语义:至多一次/至少一次/精确一次
管道基础52 条

投递语义:至多一次/至少一次/精确一次

三种投递语义描述故障时的重复与丢失边界:至多一次可能丢、至少一次可能重、精确一次要求端到端不丢不重,通常靠事务或幂等落库实现。

三种投递语义是什么

投递语义描述的是「故障发生时,数据会不会丢、会不会重」。至多一次(at-most-once)先记录位点后处理,进程崩溃时消息不再重放,可能丢;至少一次(at-least-once)先处理成功再记位点,崩溃后从旧位点重放,可能重;精确一次(exactly-once)要求端到端不丢不重,通常靠事务提交或幂等写入实现。

端到端为什么最难

引擎内部做到不丢不重相对容易,难的是把保证延伸到外部系统。例如「Kafka 里的事务 + MySQL 里的一次 insert」跨了两个系统,中间没有共同的事务协调者,只能靠幂等键、事务表或分布式事务补偿来收敛。

基本信息

至多一次故障时可能丢,实现最简单
至少一次故障时可能重,工程上最常见
精确一次不丢不重,需要事务或幂等
前提条件位点记录的时机决定重复与丢失的边界
常见折衷至少一次投递 + 下游按主键 upsert

三种语义对照

语义故障时的行为实现手段
at-most-once先提交位点再处理,崩溃后消息不再重放自动提交 + 不重试,允许丢失的埋点场景
at-least-once处理后提交位点,崩溃后重复消费手动提交 + 下游幂等,数仓链路默认选择
exactly-once端到端不丢不重Kafka 事务、两阶段提交 sink、主键 upsert

典型使用场景

  • 页面埋点、监控采样:少量丢失可接受,用至多一次换吞吐与简单
  • 数仓 ODS 落地:至多一次会丢数据,必须选至少一次并保证下游可去重
  • 账务、计费、库存扣减:重复入账后果严重,要么精确一次,要么业务层用幂等键兜住
  • 指标类流任务:位点回退会造成重复累加,用主键 upsert 收敛到精确一次效果
  • 审计与对账链路:宁可重复也不可丢,并在对账环节暴露重复

Kafka 消费侧相关配置

Kafka 消费侧相关配置
isolation.level=read_committed      # 只读已提交的事务消息,读未提交会看到被回滚的数据
enable.auto.commit=false            # 关掉自动提交,改为处理成功后手动提交
max.poll.records=500                # 控制单次 poll 量,缩短处理时间、降低再均衡风险

至少一次 + 幂等落库的组合

至少一次 + 幂等落库的组合
-- 重复消费同一条事件时结果不变,等于把语义收敛到“不丢不重”
INSERT INTO dwd_order (order_id, amount, updated_at)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE amount = VALUES(amount), updated_at = VALUES(updated_at);
幂等与事务不是一回事:幂等是「重复执行结果不变」,事务是「多次读写要么全成功要么全失败」。精确一次通常两者都要,先有幂等键,再谈事务边界。
引擎文档里宣称的精确一次,多数只覆盖引擎内部与其自带的 sink。一旦落到自写的数据库写入逻辑,保证就断了,别把产品的宣传语当成链路结论。

易错点

  • 至少一次投递配无幂等的下游,故障重放直接变成重复入账,必须在落库端做去重或 upsert
  • 开了自动提交又处理耗时较长,rebalance 时位移已推进但数据没处理完,造成静默丢数
  • 把「位点回退」当成故障处理手段:回退后消息会重放,除下游幂等外没有任何东西能拦住重复
  • 只在上游做去重而不在落库端做,重跑任务时仍会产生重复行,去重要落在最靠近存储的一层

数据来源与参考

  • Apache Kafka 官方文档:Message Delivery Semantics
  • Apache Flink 官方文档:End-to-End Exactly-Once
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌