先记住这个答案
Kafka 消费者提交 offset 的时机决定投递语义。先提交后处理,消费者崩溃时会丢消息(at-most-once);先处理后提交,崩溃时可能重复(at-least-once)。默认应采用后者,并在处理成功后同步或异步提交。若要追求精确一次,需结合事务和幂等。
- 处理后提交实现 at-least-once
- 先提交后处理实现 at-most-once
- 幂等或事务可弥补重复副作用
提交偏移量如何定义消息所有权
Kafka 消费者通过提交 offset 记录自己已消费的位置。下次 rebalance 时,新消费者从该 offset 继续拉取。若在业务处理前提交,消费者崩溃后新消费者从已提交位置开始,未处理的消息被视为已消费,造成数据丢失。
若在业务处理后提交,消费者崩溃后新消费者从上次提交位置重新拉取,会看到已处理但未提交的消息,产生重复。因此提交时机本质是选择在崩溃场景丢消息还是重复处理。
订单支付回调中先提交后处理导致丢单
假设某支付服务消费支付成功事件,更新订单状态并通知用户。设 enable.auto.commit=false,代码先调用 commitSync() 再执行业务逻辑。某次消费者处理消息时进程崩溃,但 offset 已提交,订单状态未更新。重平衡后该消息不再被消费,造成支付成功但订单显示未支付。
正确做法是先更新订单数据库并发送通知,成功后提交 offset。若在两步之间崩溃,可能重复执行更新和通知,但需在业务侧做幂等或使用事务。此场景中丢单不可接受,故必须后提交。
何时可以先提交或需要特殊处理
若业务是纯监控或日志采集,可容忍丢失,先提交能减少重复处理开销。但绝大多数业务要求 not lose,必须后提交。注意后提交也可能因处理成功但提交失败而重复,因此要处理提交失败逻辑。
使用 commitAsync 时要注意其提交失败不会自动重试,可能造成重复,但通常配合回调处理。若想精确一次,需结合 Kafka 事务或消费幂等,但事务只能保证原子性,不能抵消外部副作用。
容易答错的地方
- “先提交可以避免重复处理”
- 实际上先提交是避免重复,但以丢消息为代价。若进程在提交后、处理前崩溃,消息永久丢失,很多业务不能接受。
- “后提交必然导致重复,所以不行”
- 后提交虽然产生重复,但至少不丢。重复可以通过幂等消费或去重消除,这比丢消息更可控。因此默认应处理后提交。
面试官还会怎么问?
处理成功后提交 offset 时如果提交失败怎么办?
若提交失败但业务已成功,消息会重复消费。可重试提交或接受重复并由业务幂等。若持续失败,通常需要停止消费以避免重复堆积。
自动提交 `enable.auto.commit=true` 是什么语义?
自动提交周期性地提交最近拉取消息的 offset,不保证处理完成。若处理时间超过间隔,可能已提交尚未处理的消息,导致崩溃后丢失。
如何用事务精确一次消费?
使用 Kafka 事务并把 offset 提交与业务结果写入同一事务,使业务提交和 offset 原子。但需配合幂等或外部副作用处理,不能自动回滚副作用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。