2. 端到端一致性模型:At-Least-Once、At-Most-Once、Exactly-Once 的定义与适用场景,以及它们在实际业务中的取舍

聊实时数仓,绕不开一个核心问题:数据到底能不能对得上?

我见过太多团队,辛辛苦苦搭了 Flink + Kafka 的 pipeline,上线第一天看着数据挺美,第二天对账发现少了几百万。排查到最后,发现是某个节点重启后重复消费了一批数据,或者丢了一批数据。

说白了,这就是一致性模型没选对。

今天咱们就把这三个模型掰开揉碎讲清楚。你理解透了,后面设计容错机制、选 checkpoint 策略、定下游消费语义,心里就有底了。

2.1 三个模型的核心定义

先给个直观的理解。假设你往河里扔漂流瓶,瓶子里装着数据。

  • At-Most-Once(最多一次):瓶子扔出去就不管了。可能到了,也可能半路沉了。你不在乎。
  • At-Least-Once(至少一次):瓶子扔出去,没收到回执就再扔一个。下游可能收到重复的瓶子,但保证不会丢。
  • Exactly-Once(精确一次):瓶子扔出去,下游确保每个瓶子只被处理一次。不丢不重。

听起来 Exactly-Once 最完美对吧?但现实是,完美是有代价的。

一句话总结:

  • At-Most-Once = 可能丢,但绝不重复
  • At-Least-Once = 可能重复,但绝不丢
  • Exactly-Once = 不丢不重,但代价最高

2.2 适用场景分析

At-Most-Once:适合「丢了无所谓」的场景

说实话,实时数仓里用这个的场景不多。但也不是没有。

我记得有一次做用户实时点击流的热力图,产品经理说「大概看看趋势就行,丢个 1% 不影响」。这种场景下,用 At-Most-Once 反而能省下不少资源。因为不需要维护状态、不需要做重试,吞吐量能拉满。

典型场景:

  • 实时监控大盘(丢几个点不影响趋势判断)
  • 非关键日志采集
  • 临时性的数据探查任务

注意:千万不要在涉及钱、库存、对账的场景里用 At-Most-Once。我曾经见过一个团队把订单量统计的 pipeline 配成了 At-Most-Once,结果半夜集群抖动丢了 2000 条订单,第二天对账对到崩溃。

At-Least-Once:实时数仓的「默认选项」

这是目前实时数仓里用得最多的模型。为什么?因为大部分场景下,丢数据比重复数据更可怕。

你想想看,重复数据可以通过下游去重来解决(比如在 Kafka 里带上唯一 ID,或者用 Flink 的 deduplication 算子)。但数据丢了,你上哪儿找去?

我个人的习惯是:只要下游有去重能力,优先选 At-Least-Once。这样上游可以放心做重试,不用担心丢数据。

典型场景:

  • 实时数仓的 ODS 层(原始数据层)
  • 日志采集链路
  • 大部分实时 ETL 任务

Exactly-Once:金融级场景的「硬要求」

这个模型听起来很美,但实现起来最复杂。它要求整个链路——从数据源、到计算引擎、到下游存储——都支持精确一次语义。

在 Flink 里,Exactly-Once 依赖两阶段提交(2PC)和端到端的 checkpoint。Kafka 作为 source 和 sink 时,配合 Flink 的 Kafka connector 可以实现端到端的 Exactly-Once。

但代价也很明显:

  • 吞吐量下降 20%-40%
  • 延迟增加(因为要等两阶段提交完成)
  • 对下游存储有要求(必须支持事务或幂等写入)

典型场景:

  • 金融交易流水
  • 实时对账系统
  • 库存扣减
  • 用户余额变更

2.3 实际业务中的取舍:没有银弹

很多刚入行的同学会问:「为什么不全部用 Exactly-Once?」

答案很简单:成本。

我参与过一个项目,初期所有 pipeline 都配了 Exactly-Once。结果发现,对于日志采集这种场景,Exactly-Once 带来的额外延迟和资源消耗完全不值得。后来我们把非关键链路切回 At-Least-Once,整体吞吐量提升了 30%,而数据质量几乎没有下降。

下面这张表是我在实际项目中总结的选型参考:

业务场景 推荐模型 核心考量
实时大屏 / 监控 At-Most-Once 允许少量丢数据,追求低延迟
实时数仓 ODS / DWD At-Least-Once 下游可去重,优先保证不丢
实时数仓 DWS / ADS At-Least-Once + 幂等写入 聚合结果可覆盖,重复影响小
金融交易 / 支付 Exactly-Once 一分钱都不能差
库存扣减 Exactly-Once 超卖就是事故
用户行为日志 At-Least-Once 丢一条日志影响不大,但别丢太多

我的建议:不要一上来就追求 Exactly-Once。先问自己三个问题:

  1. 数据丢了会出事故吗?
  2. 数据重复了下游能处理吗?
  3. 为了 Exactly-Once 多花的资源,业务上能接受吗?

如果三个问题里有两个答案是「否」,那 At-Least-Once 可能就是你的最佳选择。

2.4 三种模型的实现原理对比

光说概念不够,咱们看看底层是怎么实现的。这里我以 Flink + Kafka 为例:

  • At-Most-Once:不开启 checkpoint,source 不维护 offset,sink 不做事务。挂了就从最新 offset 开始消费,中间的数据直接丢了。
  • At-Least-Once:开启 checkpoint,source 定期保存 offset。任务挂了重启后从上次 checkpoint 的 offset 开始消费,但可能重复处理 checkpoint 到故障之间的数据。
  • Exactly-Once:开启 checkpoint + 两阶段提交。sink 在 checkpoint 完成前不提交数据,确保数据要么全部写入,要么全部不写。

下面这张图展示了三种模型在 Flink 中的处理流程差异:

三种一致性模型在 Flink 中的处理流程对比 Source 读取 故障点 正常处理 重复处理 丢失数据 At-Most-Once 故障 故障后从最新 offset 开始消费,中间数据丢失 At-Least-Once CK CK 故障 ← 回退到最近 CK 回退到最近 checkpoint,重复处理部分数据,但不丢数据 Exactly-Once CK CK 2PC 故障 ← 回退到最近 CK 2PC 保证未完成事务回滚,数据既不丢也不重 关键差异: • At-Most-Once:无 checkpoint,故障即丢数据 • At-Least-Once:有 checkpoint,故障回退重放,可能重复 • Exactly-Once:checkpoint + 2PC,故障回退且事务回滚,精确一次

2.5 避坑指南:我踩过的那些坑

讲了这么多理论,最后分享几个真实教训。

坑一:以为 Exactly-Once 能解决所有问题

我曾经在一个项目里,要求所有 pipeline 都配 Exactly-Once。结果发现,下游的 MySQL 不支持事务,sink 写进去的数据根本没法回滚。最后所谓的 Exactly-Once 只是 Flink 端自己觉得是精确一次,下游该重复还是重复。

教训:端到端的 Exactly-Once 需要整个链路都支持。光 Flink 自己精确一次没用,下游存储也得配合。

坑二:At-Least-Once 不做下游去重

有一次做实时数仓的 DWD 层,用了 At-Least-Once,但下游的 ClickHouse 表没设去重逻辑。结果集群重启一次,数据就多了一倍。对账的时候发现 GMV 翻了一番,老板差点以为业务暴涨了。

教训:用 At-Least-Once 一定要在下游做好幂等或去重。ClickHouse 可以用 ReplacingMergeTree,Kafka 可以在消息里带唯一 ID。

坑三:高估了 Exactly-Once 的吞吐

有个实时大屏项目,数据量每秒十几万条。一开始配了 Exactly-Once,结果延迟从秒级飙到了分钟级。后来发现,大屏展示的数据允许少量偏差,果断切回 At-Least-Once,延迟立马降下来了。

教训:Exactly-Once 不是免费的。在选型前,先问问业务:「你能接受多少延迟?」

我的最终建议:

实时数仓里,80% 的场景用 At-Least-Once 就够了。剩下 20% 里,15% 可以用 At-Least-Once + 下游去重来近似实现 Exactly-Once。真正需要原生 Exactly-Once 的,可能只有 5%。

别为了追求理论上的完美,把系统搞复杂了。简单、可靠、可运维,比「精确一次」这个标签更重要。