Kafka 高并发下如何避免重复消费
这题非常高频,而且很容易答浅。
很多人只会说一句:
做幂等。
但这在面试里通常不够,因为面试官真正想听的是:
- 为什么会重复消费
- Kafka 自己能保证到什么程度
- 业务端该怎么分层解决
- 你在真实项目里会怎么落
一、先讲本质:为什么 Kafka 会重复消费
Kafka 默认更偏 at-least-once,也就是:
- 它更倾向“尽量别丢”
- 但不承诺“绝不重复”
所以高并发下重复消费不是异常,而是常态之一。
常见触发场景
- 业务处理成功了,但 offset 还没提交
- offset 提交失败
- consumer 宕机重启
- consumer group rebalance
- 下游超时后触发重试
- 网络抖动导致客户端重拉
这题面试里第一句怎么说更稳
Kafka 高并发下重复消费是常态,不是异常,因为它默认更偏 at-least-once 语义。
这句话一出来,面试官通常会觉得你不是把它当成“偶发 bug”在看。
二、为什么不能只指望 Kafka 本身解决
很多人会说:
- Kafka 不是有事务吗
- Kafka 不是有 exactly-once 吗
这时候你要把边界讲清楚。
更稳的回答
Kafka 的 exactly-once 更偏 Kafka 体系内部的生产和消费事务语义, 但一旦消息落到你自己的业务系统,比如:
- MySQL
- Redis
- 外部支付接口
- 积分发放
- 会员权益
这些外部系统并不会因为 Kafka 开了事务,就天然变成 exactly-once。
面试里一句很加分的话
Kafka 能保证消息传输层的一部分语义,但业务结果是否只生效一次,最终还是要靠业务幂等设计。
三、真正稳的思路:接受重复,保证业务只生效一次
这题最核心的一句话是:
不是让消息绝不重复,而是让消息即使重复到达,业务也只生效一次。
这比单纯说“避免重复消费”更专业,因为你承认现实约束,同时把重点放在结果控制上。
四、业务侧一般怎么做
通常我会分 4 层去做。
1. 先定义业务唯一键
这是第一步,也是最重要的一步。
没有唯一键,后面很难做真正稳定的幂等。
常见唯一键
- 订单号
order_id - 支付流水号
payment_id - 业务事件 ID
event_id - 退款单号
refund_id - 用户行为事件唯一 ID
面试里怎么说
我会先定义业务唯一键,因为你不先定义“什么叫同一条消息”,后面的幂等根本无从落地。
2. 用 Redis 或数据库做第一层去重
Redis 做法
常见是:
SETNX- 带过期时间
- 做短期快速去重
适合:
- 高频非核心链路
- 行为事件
- 短期防重
但 Redis 不应该单独扛最终一致性。
数据库做法
常见是:
- 唯一索引
- 幂等表 / 去重表
- 事件消费表
例如:
event_id唯一(business_type, business_id)唯一
更适合:
- 支付
- 订单
- 积分
- 权益发放
面试里怎么说更稳
如果是高频非核心链路,我会让 Redis 做第一层快速去重;如果是订单、支付、积分、会员权益这种核心链路,我会让数据库唯一约束和事务做主防线。
3. 用状态机或条件更新挡住重复生效
即使消息重复了,也不能让状态被反复改坏。
例子
订单支付:
- 只能
pending -> paid - 已经
paid的订单,再来一条支付成功消息,不应该再重复处理
退款:
- 只能从已支付进入退款中或已退款
- 不能让旧消息把状态回滚回去
为什么这层重要
因为有些场景即使第一层去重没挡住,状态机也能挡住“重复生效”。
面试里怎么说
幂等不只是去重表,还要配合状态机和条件更新。因为有些消息哪怕重复进来了,只要状态机定义得对,最终结果也不会被打乱。
4. offset 在业务成功后再提交
这块是 Kafka 面试里特别爱追问的点。
为什么
如果你先提交 offset,再处理业务:
- 业务处理中 consumer 崩了
- 这条消息就可能丢掉
如果你先处理业务,成功后再提交 offset:
- 最多是重复消费
- 但不会轻易丢消息
更稳的理解
Kafka 里通常要在“丢消息”和“重复消费”之间取舍。 更常见、更可控的选择是:
- 接受重复消费
- 通过幂等确保结果只生效一次
五、项目里更完整的落地组合
如果是核心业务链路,我通常会这样组合:
- 定义唯一业务键
- 数据库唯一约束做第一层幂等
- 状态机 / 条件更新做第二层保护
- 业务成功后再提交 offset
- Redis 作为辅助防抖或热点去重
一句话总结
Redis 负责快,数据库负责稳,状态机负责不让旧消息把结果改坏。
六、如果面试官问:高并发下为什么还会重复
你可以直接这样答:
因为高并发下 consumer 重启、rebalance、offset 提交时机、重试机制都会把重复放大。Kafka 默认更偏至少一次,所以重复消费本身是系统设计时就必须接受的前提。
七、如果面试官问:只用 Redis 锁行不行
更好的回答是:
不够。Redis 锁更多解决并发协调问题,但不能单独保证业务只生效一次。真正的主防线还是业务唯一键、数据库幂等和状态机。
这句话能把你和“只会说加锁”的人区分开。
八、如果面试官问:Kafka 不是有 exactly-once 吗
推荐回答:
Kafka 的 exactly-once 更偏 Kafka 内部链路的事务语义,不等于你的 MySQL、Redis、外部接口也天然 exactly-once。只要业务结果落到外部系统,最终还是要靠幂等设计。
这是很加分的一段。
九、30 秒版怎么答
Kafka 高并发下重复消费是常态,不是异常,因为它默认更偏 at-least-once。我的处理思路不是追求消息绝不重复,而是让消息即使重复到达,业务也只生效一次。通常会先定义业务唯一键,再用 Redis 或数据库做幂等,核心链路主要靠数据库唯一约束、事务和状态机,offset 在业务成功后再提交。
十、1 分钟版怎么答
Kafka 高并发下重复消费很常见,比如 consumer 重启、rebalance、业务处理成功但 offset 还没提交、提交失败重试,都会导致同一条消息再次被消费。所以我一般不会把目标设成“绝不重复”,而是让消费逻辑天然幂等。通常第一步先定义业务唯一键,比如订单号、支付流水号、事件 ID;第二步通过 Redis 或数据库唯一约束做去重;第三步对核心状态用状态机或条件更新,防止重复消息把状态打乱;第四步业务成功后再提交 offset。这样即使消息重复消费,最终业务结果也只会生效一次。
十一、3 分钟版怎么答
我理解 Kafka 重复消费是它语义的一部分,不是偶发 bug。因为它默认更偏 at-least-once,高并发下 consumer 宕机、group rebalance、offset 提交失败、业务处理成功但还没提交 offset,这些都会让消息再次投递。
所以这题我一般分三层答。第一层,先定义业务唯一键,比如订单号、支付流水号、退款单号、事件 ID,因为如果连“什么叫同一条消息”都没定义,幂等就没法做。第二层,用 Redis 或数据库做去重。如果是行为事件这种高频非核心链路,我会让 Redis 做第一层快速防重;如果是订单、支付、积分、会员权益这种核心链路,我会让数据库唯一约束和事务做主防线。第三层,用状态机和条件更新保护主状态,比如订单只能 pending 变 paid,已经 paid 的订单再来重复消息也不能重复改。
另外 offset 一般会在业务成功后再提交,因为如果先提交 offset 再处理业务,consumer 崩了就容易丢消息。我的思路通常是接受重复消费,但不接受重复生效。Kafka 自己能提供的是消息层的一部分语义,真正的业务 exactly-once 还是要靠幂等设计来完成。
十二、面试里最容易答差的地方
最容易答差的方式通常是:
- 只说“做幂等”
- 只说“加 Redis 锁”
- 只说“Kafka 能保证”
- 只说“唯一索引就行”
这些都太薄。
更稳的回答一定要把:
- 重复来源
- 语义边界
- 幂等设计
- offset 提交时机
这四层讲出来。
十三、最适合你的项目化表达
你可以这样讲:
像支付回调、行为回传、积分发放这种消息链路,我不会把重点放在“怎么让 Kafka 永远不重复”,而是放在“即使重复消费,业务也只生效一次”。通常我会先定义业务唯一键,再用数据库唯一约束和状态机保证主状态稳定,Redis 更多做辅助防抖或热点去重。这样在高并发和重试条件下,系统还是可控的。
十四、一句话总结
Kafka 解决的是消息传输,幂等解决的是业务结果。高并发下真正稳的方案,不是让消息绝不重复,而是让重复消息永远不能把业务结果打乱。