日期:2026-09-27
标签:#面试 #场景设计 #消息队列 #Kafka
难度:中等
来源:牛面场景题
答案说明:独立整理(站内题目标记为 VIP,未读取会员答案)
一句话答案
Kafka 链路通常允许重复投递;真正要保证的是业务副作用幂等,通过稳定业务键与原子去重,使同一事件重放也只生效一次。
面试口语版(约 75 秒)
我不会承诺消费者绝不会再次读到同一条消息。消费者处理完业务后、提交位点前宕机,重启就可能重读;生产者重试和上游重复发事件也可能带来重复。生产者幂等能限制其发送重试造成的重复写入,但不能覆盖业务侧的重发。消费端应关闭自动位点提交,定义事件 ID 或业务唯一键,在同一个数据库事务里完成“检查并登记已处理”与业务更新,最后由应用提交 Kafka 位点。若事务提交后位点提交失败,重读时唯一约束让更新成为空操作。Kafka 事务能覆盖 Kafka 内部的读、处理、写与位点原子提交;外部数据库和第三方接口要用自身的唯一约束、条件更新或幂等键。
机制与伪代码
sequenceDiagram
participant K as Kafka
participant C as 消费者
participant D as 业务数据库
K->>C: 交付事件 E
C->>D: 原子登记 E 并更新业务
D-->>C: 提交成功
Note over C,K: 位点提交前宕机,重启后 E 会重读
K->>C: 再次交付 E
C->>D: 唯一键判重,不再更新
C->>K: 确认已处理后提交位点
收到事件 e:
关闭自动位点提交,由应用控制提交时机
开始数据库事务
尝试插入 (consumer_name, e.event_id) 到已处理事件表,唯一约束防重
如果插入成功:执行业务更新,并提交数据库事务
如果唯一键已存在:确认之前已处理,结束数据库事务
只有数据库事务成功或确认已处理后,才提交 Kafka 位点
其他错误:不提交位点,按策略重试或隔离并告警
伪代码要求去重记录与业务副作用在同一个可用的事务边界中;仅靠 Redis 临时标记或先查后写,遇并发和崩溃可能失效。不同数据库的冲突处理语法需按实际产品实现。
具体失败分支
订单支付事件触发发券。发券事务提交后消费者宕机,位点没有提交;重启再收到同一事件,唯一键“活动 ID + 订单 ID + 券种”冲突,发券逻辑不再执行。若调用的是第三方发券 API,本地去重记录与外部调用不能天然原子提交,应向第三方传幂等键并做结果查询、对账或补偿。
取舍与易错点
- 采用上述处理后提交位点的流程时,设置
enable.auto.commit=false,由应用明确决定提交时机。 - 生产者幂等、Kafka 事务、消费者幂等分别解决不同范围的问题;不要把 Kafka 的 exactly once 扩展成“任何外部数据库都只写一次”。
- 去重键必须稳定且粒度正确;同一业务事件多种处理动作,可把消费者名称或动作类型纳入键。
- 幂等记录的保留期至少覆盖消息可重放和人工补偿窗口;清理过早会让旧事件重新生效。
- 条件更新适合状态单调前进的场景,如仅把“待支付”改为“已支付”;副作用复杂时用事务性唯一流水更清楚。
面试官递进追问
- 为什么重复消费不可完全避免? 业务提交与位点提交之间存在崩溃窗口。
- 生产者 enable.idempotence 能替代消费端去重吗? 不能,它只约束生产者重试写入的范围。
- 调用第三方 API 如何防重? 传稳定幂等键,配状态查询、超时重试与对账;若对方不支持,需设计可补偿流程。
自测
- 按时间顺序演示“业务成功,位点未提交”后为何重放。
- 设计发券的唯一键,并解释为什么只用消息 offset 可能不够。
- 说清 Kafka 内部事务和外部数据库事务的边界。
参考资料
核对日期:2026-09-27。