Kafka 出现重复消费,怎么保证幂等?
面试怎么回答
我会先定义业务唯一键,区分 Kafka 记录重放和上游重复业务事件。消费侧通常先完成业务写入再提交 Offset,接受故障窗口内可能重放,并在下游用唯一约束、UPSERT、版本号或 Idempotency Key 保证结果幂等。如果下游支持,就把结果和消费位置放进同一事务;Kafka 到 Kafka 可使用事务。最后还会监控去重命中和重放来源,确保人工回放时幂等规则仍然有效。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
很多人会回答:
“关闭自动提交,业务处理成功后再手动提交 Offset。”
这个顺序能降低消息丢失风险,但不能消除重复消费。
因为业务写入成功之后、Offset 提交成功之前,进程仍然可能崩溃。任务恢复后会从旧 Offset 重新读取,刚才的消息就会再次处理。
所以真正要保证的是:
允许消息重放,但同一个业务动作重复执行时,结果不能被重复放大。
第一步:先定义什么叫“同一条业务事件”
幂等设计的第一步不是写 Redis,而是确定唯一键。
常见选择包括:
- 上游生成的全局
event_id order_id + event_typebusiness_id + version- Topic、Partition、Offset 组成的传输位置
Topic、Partition、Offset 可以识别 Kafka 中同一条记录是否重放,但不能识别上游把同一个业务动作重新发送成另一条 Kafka 记录。
涉及订单、支付、积分和状态变更时,更可靠的是使用稳定业务 ID,再结合事件类型或版本定义唯一动作。
第二步:在结果落地层实现幂等
不同下游,幂等方式不同。
数据库写入
可以使用唯一约束、UPSERT、条件更新或版本号控制。
例如积分发放表对 (order_id, action_type) 建唯一约束。同一订单的“支付成功发积分”无论收到多少次,最终只允许写入一次。
缓存或去重表
可以用 SET NX、去重表或状态存储记录已处理 ID,但必须设计 TTL、容量、原子性和故障恢复。
TTL 太短,历史消息重放时仍可能重复;TTL 太长,则要承担持续增长的存储成本。
外部接口调用
调用支付、优惠券或通知服务时,应传递稳定的 Idempotency Key,让服务端识别重复请求。只在 Consumer 本地记录成功状态,进程切换后并不可靠。
第三步:正确理解业务写入和 Offset 的顺序
如果先提交 Offset,再写业务结果,写入失败时可能丢数据。
如果先写业务结果,再提交 Offset,进程在两者之间失败时可能重复处理。
因此通常选择:
先完成业务写入 → 再提交 Offset → 下游保证幂等
这是一种“宁可重放,不要丢失”的 At-Least-Once 思路。
如果下游存储支持,可以把业务结果和消费位置放进同一个事务。Kafka 到 Kafka 的处理可以利用事务把输入 Offset 和输出消息原子提交;写外部数据库时,则需要目标系统配合,不能只靠一个 Kafka 参数承诺端到端 Exactly-Once。

第四步:把重放能力也纳入设计
生产环境里,重复不只来自进程崩溃,还可能来自:
- Rebalance 后从已提交位置恢复
- Consumer 超时导致分区转移
- 人工重置 Offset 或历史重放
- 上游重试发送同一业务事件
- 下游超时后结果未知,Consumer 主动重试
因此还要监控去重命中数、重复事件来源、写入冲突和异常重试,并明确人工重放时是否继续沿用原来的幂等键。
真正可用的幂等方案,必须经得起故障恢复和历史回放,而不只是正常运行时不重复。
一个典型场景
比如订单支付成功后发放积分。Consumer 已经把积分写入数据库,但在提交 Offset 前进程退出。
新 Consumer 接管分区后重新读取这条消息。如果逻辑只是执行 积分 = 积分 + 100,用户会得到两次积分。
改造后,积分流水使用 (order_id, action_type) 唯一键,并在同一事务里写积分流水和更新账户余额。重复消息再次到达时命中唯一约束,直接返回已处理结果,再提交 Offset。
不是让 Kafka 永远不重复,而是让重复消息无法制造重复业务结果。