1、Exactly Once 概述:什么是 Exactly Once、为什么需要 Exactly Once、端到端 Exactly Once 的挑战
各位同学,咱们今天正式开始聊 Exactly Once。这个词在分布式系统圈子里,可以说是既熟悉又神秘。熟悉是因为几乎每个做消息、做流计算的工程师都听过它;神秘是因为真正把它落地到生产环境的人,其实不多。
我最早接触 Exactly Once 是在做实时数仓的时候。当时团队选型了一套流处理框架,文档上赫然写着「支持 Exactly Once 语义」。结果上线第一天,数据对账就差了千分之三。嗯,后来我才明白——文档里的 Exactly Once 和实际跑出来的 Exactly Once,中间隔着一整个银河系。
什么是 Exactly Once
先给个最直白的定义:Exactly Once 指的是每条数据在整个处理链路中,恰好被处理一次,不多也不少。
你想想看,在分布式系统里,消息传递通常有三种语义:
| 语义 | 含义 | 典型场景 |
|---|---|---|
| At Most Once | 最多一次,丢了就丢了,不重试 | 日志采集、监控指标(丢几条无所谓) |
| At Least Once | 至少一次,保证不丢,但可能重复 | 支付扣款(必须执行,重复了再对账) |
| Exactly Once | 恰好一次,不丢不重 | 金融交易、库存扣减、积分变动 |
说白了,At Most Once 是「佛系投递」,At Least Once 是「死磕投递」,而 Exactly Once 是「精准投递」。但这里有个坑——Exactly Once 并不是一种传输协议,而是一种端到端的语义保证。它需要上下游、中间件、业务逻辑一起配合才能实现。
核心认知:Exactly Once 不是某个组件的能力,而是整个系统的契约。任何一个环节掉链子,这个语义就破了。
为什么需要 Exactly Once
有人可能会问:「重复一条数据能怎样?大不了我下游做去重嘛。」
我举个例子你就明白了。假设你做一个积分系统,用户每消费 1 元得 1 积分。如果消息重复了 3 次,用户账户里就多了 2 个不该有的积分。单个用户看起来问题不大,但如果是双十一大促,几亿条消息重复个 0.1%,那就是几十万的积分错账。财务对账的时候,你猜谁背锅?
再说一个我踩过的坑。之前做一个订单状态同步系统,订单从创建到完成要经过 6 个状态变更。因为用了 At Least Once 的消息队列,同一个「订单已支付」事件被消费了两次。结果呢?订单状态从「已支付」变成了「已支付」——看起来没变,但状态机的版本号被覆盖了,后续的「已发货」事件反而被丢弃了。最后用户付了钱没收到货,客服电话被打爆。
所以,需要 Exactly Once 的场景通常有三个特征:
- 数据敏感:重复或丢失都会造成直接经济损失
- 状态累积:多次执行会产生叠加效应(积分、库存、余额)
- 审计要求:需要和外部系统对账,不能有偏差
我的建议:不要在所有场景都追求 Exactly Once。如果你的业务允许「最终对账修正」,用 At Least Once + 幂等性就足够了。Exactly Once 是有成本的,后面我们会详细算这笔账。
端到端 Exactly Once 的挑战
好,现在问题来了:既然 Exactly Once 这么好,为什么很多系统不直接支持?
因为端到端的 Exactly Once 太难了。我把它拆成三个层面的挑战,你感受一下:
1. 生产端:消息可能重复发送
生产者发送消息到 Broker,网络超时了怎么办?你重试,Broker 可能已经收到了,于是重复。你不重试,消息可能真的丢了。这就是经典的「生产端重复」问题。
2. 传输端:Broker 可能丢消息或重复投递
Kafka、Pulsar 这些消息中间件,为了保证高吞吐,内部做了很多异步和批处理。一旦节点宕机,未刷盘的消息可能丢失;Leader 切换时,消费者可能收到重复的消息。我见过一个案例,Kafka 在 Controller 切换期间,同一个分区的消息被重复消费了 4 次。
3. 消费端:处理成功但提交失败
这是最隐蔽的坑。消费者处理完消息,写入了数据库,但在提交 Offset 之前挂了。重启后,消息被重新消费,数据库里就多了一条重复记录。你可能会说「我用事务啊」,但分布式环境下的跨系统事务,成本高得吓人。
这三个挑战叠加起来,就构成了一个「不可能三角」的假象:高吞吐、低延迟、Exactly Once,你最多只能选两个。
我曾经踩过的坑:在一个实时风控项目中,我们试图用分布式事务来实现 Exactly Once。结果事务协调器成了瓶颈,P99 延迟从 50ms 飙升到 2s。最后不得不放弃,改用「At Least Once + 幂等去重」的方案。所以,技术选型一定要结合业务容忍度,不要为了语义完美而牺牲可用性。
为了让你更直观地理解这些挑战之间的关系,我画了一张图:
从这张图你可以看到,三个环节各自有各自的痛点,而且它们之间还会相互放大。生产端的重复会加剧消费端的去重压力,传输端的丢消息又迫使生产端加大重试力度,最终形成一个恶性循环。
那么,怎么破局?
我个人习惯把 Exactly Once 的落地拆成四个关键能力:
- 幂等性:同一个操作执行多次,结果和执行一次一样
- 事务性:跨多个系统的操作要么全部成功,要么全部回滚
- 去重机制:在消费端识别并过滤重复消息
- 状态持久化:把处理进度和结果持久化,支持故障恢复
这四个能力,每一个都有很多技术细节和坑。后面的章节,我会带你一个一个地拆解,结合我在生产环境中的真实案例,告诉你哪些方案靠谱,哪些方案看起来很美但实际跑不动。
一句话总结本章:Exactly Once 不是一种技术,而是一种系统级的契约。它需要生产端、传输端、消费端三方协同,通过幂等、事务、去重、持久化等手段,共同保证每条数据恰好被处理一次。理解了这个前提,我们才能继续往下聊具体的落地方法。