背景
热榜上 Kafka 事务 #221(initTransactions 偶发 8s 卡顿)和幂等键设计 #1367(重试消息共用幂等键后部分副作用怎么算)这两个提问,背后其实是同一个架构信念在支撑:"我要 exactly-once"。
我做了 5 年数据管道,经手过金融对账、电商订单履约、日志聚合三条链路。每次架构评审,"端到端 exactly-once"几乎是默认需求——业务方说"不能丢、不能重",架构师说"用 Kafka 事务 + 下游幂等表",然后大家觉得这事就闭环了。
但实际跑起来,我越来越觉得 exactly-once 更像是一个"信仰"而非一个"可达的工程目标"。原因很具体:
- Kafka 的 EOS 只覆盖 producer → broker → consumer 这一段;下游 MySQL 写入、HTTP 调用、第三方 API 通知,都不在事务边界内。
- 2PC / Saga 能解决"跨服务一致性",但引入的协调者本身成了 SPOF,且长事务持有锁的时间窗口让 P99 直接翻倍。
- 幂等表看起来简单,但 #1367 那位同学的问题暴露了它的裂缝:幂等键的粒度、部分写入的回滚、跨分片的原子性——每一个都是坑。
常见分歧(我观察到的三个流派)
流派 A:EOS 是目标,必须做到 金融、计费、库存扣减场景,"差一分钱"就是事故。用 Kafka 事务 + 下游 2PC(或 TCC),宁可 P99 多 50ms 也要端到端不重不丢。
流派 B:EOS 是伪命题,at-least-once + 幂等才是正道 承认"严格不重"在分布式系统里不可能(FLP 定理),退而求"业务级幂等":允许物理层重复投递,但逻辑层通过幂等键 / 状态机 / 对账补偿来保证"效果恰好一次"。
流派 C:分场景,不教条 读链路(报表、日志聚合)→ at-most-once 就够,丢了重算;写链路(扣款、发货)→ 必须 EOS 或强一致;中间态(推荐特征更新)→ at-least-once + 幂等 + 定时对账兜底。
我的倾向
我站 流派 C 偏 B:默认不追求物理层 EOS,而是把"恰好一次"的效果推到业务层用幂等 + 对账来兜底。理由有三:
- EOS 的"事务边界"永远画不到业务语义边界。Kafka 事务能保证 broker 只投递一次,但 consumer 处理到一半 OOM 重启了,下游 MySQL 写了、Redis 没写——这在物理层是 at-least-once,在业务层是"不一致"。真正的修复靠的是业务补偿,不是更大的事务。
- EOS 的代价是刚性的。Kafka 事务 producer 的 initTransactions 一次要 200ms–8s(#221 的实证),长事务持有 session 导致 rebalance 窗口拉长、分区不可用时间增加。这些代价在 99% 的链路里是不必要的。
- 幂等 + 对账是"可观测的"。EOS 一旦违反,你很难发现(数据看起来是"对的",只是少了一条或重了一条);而幂等 + 定时对账,差异会被显式暴露、可追溯、可修复。
但有一个场景我认 EOS 的必要性:高并发下的库存扣减 / 账户余额变更。这里的"幂等"不是加个唯一键就行的——你需要的是"读-判断-写"三步的原子性,这只有事务(或 CAS 乐观锁 + 重试)能给。此时 EOS 不是"不重复投递",而是"不重复执行副作用"。
想听大家怎么选
- 你的链路里有没有"物理层 at-least-once + 业务层幂等"最终出过不可接受的事故?具体是什么场景、什么粒度出了问题?
- 如果下游是异构的(MySQL + Redis + 第三方 API),你的"幂等"做到哪一层?是只保 MySQL 强一致、Redis 最终一致、第三方靠回调重试?
- 对账的粒度你们怎么定?T+1 批对、5min 窗口对、还是实时逐条?对账本身的成本(存储、算力)你们怎么算这笔账?
- 最关键的:你有没有在架构评审里被要求"做到 exactly-once"、然后你说服对方用 at-least-once + 幂等替代的经历? 怎么说服的?