Kafka 实现 Exactly Once Semantics(EOS,精确一次语义) 是通过结合以下三个关键机制来达成的:
1. 幂等生产者(Idempotent Producer)
-
目的:防止生产者重试导致消息重复写入。
-
原理:
- 每个生产者被分配一个唯一的
producer.id(PID)。 - 每条消息附带一个单调递增的序列号(sequence number)。
- Broker 端会为每个
<PID, 分区>维护一个序列号窗口,如果收到重复或乱序的消息,会丢弃或拒绝。
- 每个生产者被分配一个唯一的
-
开启方式 :设置
enable.idempotence=true(默认 false)。 -
效果 :保证单个分区内的写入是 Exactly Once(在单个会话内)。
注意:幂等性只在单个生产者会话、单个分区级别有效,不跨分区或跨会话。
2. 事务(Transactions)
-
目的:实现跨分区、跨主题的原子写入,同时支持消费-处理-生产的原子性(即读取一批消息、处理、再写入新主题,整个流程要么全部成功,要么全部失败)。
-
核心组件:
- 引入了 Transaction Coordinator(协调器)。
- 使用
transactional.id标识生产者事务(即使重启也能恢复状态)。
-
关键 API:
scssjava 编辑 producer.initTransactions(); producer.beginTransaction(); // send messages producer.sendOffsetsToTransaction(offsets, groupId); // 提交消费偏移量(用于 consumer-producer chain) producer.commitTransaction(); // 或 abortTransaction() -
应用场景:典型的 Kafka Streams 或"消费-转换-生产"管道。
事务 + 幂等性 = 跨分区的 Exactly Once 写入。
3. 消费者端的偏移量提交与处理原子化
-
问题:传统方式中,先处理消息再提交 offset,若处理完但提交前崩溃,会导致重复消费;反之则可能丢失消息。
-
解决方案:
- 将 offset 提交 作为事务的一部分,与处理结果一起原子提交。
- 使用
sendOffsetsToTransaction()将 offset 发送给事务协调器,随事务一起 commit。
-
要求:
- 消费者必须使用
isolation.level=read_committed(默认read_uncommitted),只读已提交事务的消息。 - 生产者需开启事务。
- 消费者必须使用
总结:Kafka 的 Exactly Once 是如何工作的?
| 组件 | 功能 | 作用范围 |
|---|---|---|
| 幂等生产者 | 防止单分区内重复写入 | 单分区、单会话 |
| 事务 | 跨分区原子写入 + 偏移量提交原子化 | 跨分区、跨主题、端到端 |
| 消费者隔离级别 | 只读已提交消息 | 保证不读"未完成"事务 |
✅ 端到端 Exactly Once 需要:
- 生产者开启幂等性和事务;
- 消费者设置
isolation.level=read_committed; - 应用逻辑将 offset 提交纳入事务(如 Kafka Streams 自动处理)。
补充说明
- Kafka Streams 内置支持 EOS,只需设置
processing.guarantee="exactly_once_v2"(推荐使用 v2,性能更好)。 - Exactly Once ≠ 全局唯一:它保证的是"每条消息被处理且仅被处理一次",但不保证全局顺序或去重业务逻辑(如重复订单仍需应用层判断)。