Kafka 高并发下如何避免重复消费

这题非常高频,而且很容易答浅。

很多人只会说一句:

做幂等。

但这在面试里通常不够,因为面试官真正想听的是:

  1. 为什么会重复消费
  2. Kafka 自己能保证到什么程度
  3. 业务端该怎么分层解决
  4. 你在真实项目里会怎么落

一、先讲本质:为什么 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 里通常要在“丢消息”和“重复消费”之间取舍。 更常见、更可控的选择是:

  • 接受重复消费
  • 通过幂等确保结果只生效一次

五、项目里更完整的落地组合

如果是核心业务链路,我通常会这样组合:

  1. 定义唯一业务键
  2. 数据库唯一约束做第一层幂等
  3. 状态机 / 条件更新做第二层保护
  4. 业务成功后再提交 offset
  5. 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 解决的是消息传输,幂等解决的是业务结果。高并发下真正稳的方案,不是让消息绝不重复,而是让重复消息永远不能把业务结果打乱。