订阅更新

Kafka 出现重复消费,怎么保证幂等?

面试问 022 约 4 分钟读完 3 次阅读

本文目录
  1. 面试怎么回答
  2. 第一步:先定义什么叫“同一条业务事件”
  3. 第二步:在结果落地层实现幂等
  4. 第三步:正确理解业务写入和 Offset 的顺序
  5. 第四步:把重放能力也纳入设计
  6. 一个典型场景

面试怎么回答

我会先定义业务唯一键,区分 Kafka 记录重放和上游重复业务事件。消费侧通常先完成业务写入再提交 Offset,接受故障窗口内可能重放,并在下游用唯一约束、UPSERT、版本号或 Idempotency Key 保证结果幂等。如果下游支持,就把结果和消费位置放进同一事务;Kafka 到 Kafka 可使用事务。最后还会监控去重命中和重放来源,确保人工回放时幂等规则仍然有效。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

很多人会回答:

“关闭自动提交,业务处理成功后再手动提交 Offset。”

这个顺序能降低消息丢失风险,但不能消除重复消费。

因为业务写入成功之后、Offset 提交成功之前,进程仍然可能崩溃。任务恢复后会从旧 Offset 重新读取,刚才的消息就会再次处理。

所以真正要保证的是:

允许消息重放,但同一个业务动作重复执行时,结果不能被重复放大。


第一步:先定义什么叫“同一条业务事件”

幂等设计的第一步不是写 Redis,而是确定唯一键。

常见选择包括:

  • 上游生成的全局 event_id
  • order_id + event_type
  • business_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。

Kafka 重复消费处理链路


第四步:把重放能力也纳入设计

生产环境里,重复不只来自进程崩溃,还可能来自:

  • Rebalance 后从已提交位置恢复
  • Consumer 超时导致分区转移
  • 人工重置 Offset 或历史重放
  • 上游重试发送同一业务事件
  • 下游超时后结果未知,Consumer 主动重试

因此还要监控去重命中数、重复事件来源、写入冲突和异常重试,并明确人工重放时是否继续沿用原来的幂等键。

真正可用的幂等方案,必须经得起故障恢复和历史回放,而不只是正常运行时不重复。


一个典型场景

比如订单支付成功后发放积分。Consumer 已经把积分写入数据库,但在提交 Offset 前进程退出。

新 Consumer 接管分区后重新读取这条消息。如果逻辑只是执行 积分 = 积分 + 100,用户会得到两次积分。

改造后,积分流水使用 (order_id, action_type) 唯一键,并在同一事务里写积分流水和更新账户余额。重复消息再次到达时命中唯一约束,直接返回已处理结果,再提交 Offset。

不是让 Kafka 永远不重复,而是让重复消息无法制造重复业务结果。


刷题进度保存在当前浏览器