60 秒回答模板

MQ 常见语义是至少一次投递,网络超时、ack 失败、消费者重启、重平衡和生产者重发都会导致重复。消费者幂等要用业务唯一键而不是盲目信任 messageId,典型流程是消费消息后开启数据库事务,先用 `business_key` 插入消费记录表或依赖业务表唯一约束,插入成功才执行业务状态变更,业务提交成功后再 ack;如果重复插入冲突,就直接按已处理返回。状态变更还要通过状态机、乐观锁或版本号保证只能从合法前置状态推进。

考点 核心机制与工程取舍
难度 中高频面试题
回答目标 按定义、机制、场景讲清楚

深入解析

01

先承认重复投递是常态

消费者处理成功但 ack 丢失、消费超时、rebalance、Broker 重投、生产端重试,都可能让同一业务事件再次到达。幂等不是优化项,而是消费端基本能力。

02

幂等键优先用业务语义

messageId 可能因重发而变化,不能总代表同一业务操作。订单号、支付单号、请求号、事件唯一 ID 这类业务 key 更可靠。

03

去重记录要和业务写入同边界

消费记录表插入和业务状态修改最好在同一个本地事务中提交。否则去重成功但业务失败,或业务成功但去重丢失,都会留下难以恢复的不一致。

04

状态机防止重复推进

扣款、发货、退款等业务要校验当前状态,只允许 `待支付 -> 已支付` 这类合法转移。重复消息到来时发现状态已推进,应安全返回而不是再次执行副作用。

05

ack 顺序决定重投风险

不能先 ack 再提交业务,否则崩溃会丢消息。正确做法是业务事务提交后 ack;ack 失败导致重投时,由幂等记录和状态机兜住重复。

易错点

  • 认为 MQ 保证不会重复投递,消费逻辑没有幂等。
  • 只用 messageId 去重,不考虑业务重发和补偿事件。
  • 先 ack 后提交业务,崩溃时造成消息丢失。
  • 去重记录和业务修改不在同一可靠边界,留下半成功状态。

面试官追问

用 Redis `setnx` 去重可以吗?

轻量场景可以,但要考虑 TTL、Redis 持久化、缓存丢失和业务 DB 事务不一致。关键资金或订单状态更适合数据库唯一约束或消费表。

消费成功但 ack 失败怎么办?

消息会重投,所以业务必须能重复执行而无副作用。重复到达时命中消费表或状态机,直接返回成功并再次 ack。

为什么不直接用 messageId 去重?

生产端重试、业务补发或跨系统转发可能产生不同 messageId,但业务上是同一操作。幂等键应该来自业务唯一语义。

去重记录表会不会无限增长?

会,所以要按业务保留期清理、按时间分区或归档。清理窗口必须大于消息最大重试和补偿周期。