一、重复消费的根本原因
核心:offset 提交时机 和 消息处理完成 不同步
-
自动提交 offset(默认)
消费者定时自动提交位移,还没处理完消息就提交了。若此时消费者宕机/重启,会从上一次已提交 offset 之后重新拉取,造成重复。
-
手动提交 offset 失败/超时
业务逻辑执行成功,但网络问题、客户端异常导致 offset 提交失败,重启后重消费。
-
消费者再均衡(Rebalance)
组内上下线、分区重分配,未提交 offset 的分区会被其他消费者接管,重新消费。
-
生产者重试
生产者发送消息超时,重试投递,服务端存多条相同消息。
-
客户端手动回滚 offset
人为将位移调回之前位置,主动触发重复消费。
二、主流语义与对应方案
Kafka 消息投递三种语义:
• At Most Once(最多一次):可能丢消息,不会重复
• At Least Once(至少一次):可能重复,不会丢消息(Kafka 默认)
• Exactly Once(精确一次):不丢、不重复(业务最终唯一)
- 实现 At Most Once(最多一次,不推荐)
逻辑:先提交 offset,再处理消息
• 流程:拉取消息 → 立即提交 offset → 执行业务
• 问题:提交完宕机,业务没执行,消息丢失
• 适用:对数据丢失不敏感的场景
- 实现 At Least Once(至少一次,通用默认)
逻辑:先处理消息,成功后再提交 offset
• 关闭自动提交,使用手动提交 offset
• 流程:拉取消息 → 执行业务逻辑 → 全部成功 → 手动提交 offset
• 特点:保证消息不丢失,但依然会出现重复消费,需业务兜底
Java 关键配置
关闭自动提交
enable.auto.commit = false
手动同步/异步提交 offset
- 实现 Exactly Once(精确一次,解决重复消费核心方案)
分两类:Kafka 原生事务 + 业务幂等(生产环境最常用组合)
方案一:业务层做幂等(推荐,成本最低)
不管消息重复多少次,多次执行结果和执行一次完全一致。
常用实现方式:
-
唯一主键去重(数据库唯一索引)
用消息自带唯一 ID(订单ID、消息ID)作为数据库主键/唯一索引,重复插入直接报错忽略。
-
全局唯一 ID + 分布式锁
消费前基于消息 ID 加锁,同一时间只处理一条;处理完成标记状态。
-
状态机判断
业务表记录消息处理状态(待处理/已完成/失败),已完成则直接跳过。
-
Redis 防重
消费前用 setnx 存入消息ID,设置过期时间,存在则直接放弃消费。
企业主流方案:手动提交offset + 业务幂等,适配绝大多数业务。
方案二:Kafka 事务 + 幂等生产者(Kafka 原生 Exactly Once)
适用于 Kafka 内部流转(topic→topic)、流式计算场景。
-
幂等生产者
开启后生产者自动去重,解决生产者重试导致的消息重复。
配置:enable.idempotence = true
-
Kafka 事务
绑定生产者、消费者到同一个事务,实现「消费-处理-生产新消息-提交offset」原子化。
适用:跨 Topic 流转、流处理(Flink/Spark Streaming)。
三、消费端最佳实践(避重复+降风险)
-
务必关闭自动提交,使用手动提交
业务处理成功再提交,区分同步提交(保证可靠)、异步提交(高吞吐)。
-
单条消费优先,谨慎批量消费
批量消费下,部分消息成功、部分失败,offset 难以精准控制,极易重复。
-
避免频繁再均衡
合理设置会话超时、心跳间隔,减少消费者上下线。
-
失败消息进入死信队列 DLQ
消费失败不要无限重试(会加剧重复),重试几次后转发到死信队列,人工排查。
-
分区内串行消费
同一个分区只用一个线程处理,不要多线程并发消费同一分区,避免顺序+重复双重问题。
四、总结速记
-
为什么重复:offset 提交晚于业务、再均衡、生产者重试。
-
基础兜底:关闭自动提交,先执行业务,后手动提交 offset。
-
根治重复:业务幂等(通用首选)。
-
流式/Topic 流转:开启幂等生产者 + Kafka 事务实现精确一次。
-
禁忌:不要先提交 offset 再执行业务,会造成消息丢失。