别再被 Exactly-Once 忽悠了:端到端一致性到底是怎么落地的?
大家好,我是Echo_Wish。
混大数据这些年,我发现一个特别有意思的现象:
凡是系统一出问题,PPT 上一定写着:Exactly-Once。
凡是真正线上跑稳的系统,反而不太爱吹这个词。
不是 Exactly-Once 不重要,而是——
大多数人压根没搞清楚:你嘴里说的,到底是不是“端到端”的 Exactly-Once。
今天这篇,我不站厂商、不念白皮书,就聊三件事:
- Exactly-Once 到底“难”在哪
- 真正的端到端 Exactly-Once 是怎么拼出来的
- 一个能落地的实战案例(不是童话)
一、先泼点冷水:Exactly-Once 从来不是一个开关
很多新同学会问我一句话:
哥,Flink 开个 exactly-once 不就完了吗?
我一般会反问一句:
你说的是哪一段?
- Source?
- Operator?
- Sink?
- 还是从 Kafka 到 MySQL 的“人生全流程”?
Exactly-Once不是一个功能点,而是一个系统级承诺。
我们先拆一句最容易被忽略的话:
端到端 Exactly-Once = 从数据产生 → 计算 → 落库,语义只生效一次
只要链路上任何一个环节掉链子,
整个“端到端”三个字,立刻作废。
二、Exactly-Once 为什么这么容易被“说假话”
我见过太多系统,实际是下面这种结构:
Kafka (至少一次) ↓ Flink(exactly-once) ↓ MySQL(普通 insert)然后对外宣称:
我们系统是 Exactly-Once
这句话一半真、一半假。
Flink内部状态确实是 exactly-once
但最终结果,很可能是:
- 重复写
- 脏数据
- 或者靠人工兜底
问题就出在一句话上:
Exactly-Once 不是“算一次”,而是“生效一次”
三、端到端 Exactly-Once 的三块基石
真正靠谱的实现,逃不开这三样东西:
1️⃣ 可回溯的 Source(通常是 Kafka)
Kafka 为什么能当大数据“祖宗”?
一句话:
Offset 是状态,不是日志。
只要你:
- 不自己乱提交 offset
- 不用 auto commit
- 让流计算框架接管 offset
那 Source 这一段,基本是稳的。
2️⃣ 有状态一致性的计算引擎(Checkpoint)
这一段 Flink 做得确实漂亮。
核心只有一句话:
状态 + offset = 原子快照
只要 checkpoint 成功:
- 状态回到过去
- offset 也回到过去
- 计算结果不会“穿越”
这一步,很多人高估了自己,也低估了 Flink。
3️⃣ 能“配合演出”的 Sink(最容易翻车)
这里是 Exactly-Once真正的修罗场。
问你一个问题:
如果 Flink checkpoint 成功了,但数据库 commit 失败了,怎么办?
你会发现:
- 数据库不知道 Flink 的 checkpoint
- Flink 不知道数据库的事务状态
所以:端到端 Exactly-Once,本质是一个“跨系统事务问题”。
四、两条路:你要“绝对正确”,还是“工程上可控”
说实话,现实世界只有两种方案。
路线一:两阶段提交(真·Exactly-Once)
典型代表:
Flink + Kafka Transaction / 支持 XA 的 Sink
思路很简单:
- Sink 先 prepare(不提交)
- Checkpoint 成功
- 再统一 commit
- 失败就 rollback
示意代码(简化版):
publicclassExactlyOnceSinkextendsTwoPhaseCommitSinkFunction<Event,Txn,Void>{@OverrideprotectedTxnbeginTransaction(){returnopenTransaction();}@Overrideprotectedvoidinvoke(Txntxn,Eventvalue,Contextcontext){txn.write(value);}@OverrideprotectedvoidpreCommit(Txntxn){txn.flush();}@Overrideprotectedvoidcommit(Txntxn){txn.commit();}@Overrideprotectedvoidabort(Txntxn){txn.rollback();}}优点:
- 语义最干净
- 理论上的 Exactly-Once
缺点:
- 实现复杂
- 对 Sink 要求极高
- 延迟和吞吐都会受影响
说句大实话:
不是核心账务系统,真没必要这么玩。
路线二:幂等 + 去重(工程上最常见)
这条路,才是大厂真正跑得最多的。
核心思想一句话:
我允许你重来,但结果不能变。
比如:
- 每条数据有唯一业务 ID
- Sink 端做 upsert / 去重
- 或者用状态表防重
示例(MySQL 幂等写):
INSERTINTOorders(order_id,amount)VALUES(?,?)ONDUPLICATEKEYUPDATEamount=VALUES(amount);或者 Flink 侧维护已处理标记:
ValueState<Boolean>seen;if(seen.value()==null){process(event);seen.update(true);}优点:
- 实现简单
- 性能好
- 可维护性强
缺点:
- 严格意义上不是数学级 Exactly-Once
- 但业务完全能接受
我个人观点很明确:
业务正确性 > 语义洁癖。
五、一个真实可落地的端到端案例
场景:订单实时统计
链路
Kafka → Flink → MySQL策略组合
| 环节 | 策略 |
|---|---|
| Source | Kafka + checkpoint 管理 offset |
| 计算 | Flink exactly-once 状态 |
| Sink | MySQL 幂等 upsert |
| 兜底 | 定期离线校对 |
核心代码逻辑(简化):
stream.keyBy(Order::getOrderId).process(newProcessFunction<>(){@OverridepublicvoidprocessElement(Orderorder,Contextctx,Collector<Result>out){out.collect(aggregate(order));}}).addSink(newJdbcUpsertSink());上线后表现:
- 宕机重启:数据不乱
- Kafka 重放:结果不翻倍
- DBA 不骂人
- 产品不焦虑
这就是工程上性价比最高的 Exactly-Once。
六、说点掏心窝子的总结
最后我想说一句可能不太“政治正确”的话:
Exactly-Once 不是信仰,是成本。
你要问我什么时候必须追求端到端 Exactly-Once?
我的答案只有一个:
当重复一次,比系统复杂十倍还贵的时候。
否则:
- 幂等
- 去重
- 校对
- 监控
这四件套,往往比“完美语义”更重要。