消息队列是后端面试的高频区,但大多数人背的是「Kafka 快、RabbitMQ 灵活」这种标签,真被问到「怎么保证不丢、不重、不乱序」就卡壳。原因很简单:丢、重、乱、积压这些事不是某个开源产品的开关,而是横跨生产、存储、投递、消费、运维五条链路的工程取舍。

本文按面试常见的七个问题逐条拆解。每问先给结论,再列最容易踩的坑,最后说清楚什么场景才用得上。读完你不一定能背下所有参数,但能建立一张「账」——无论被问哪种 MQ,你都知道该从哪几笔算起。


一、选型:Kafka、ActiveMQ、RabbitMQ、RocketMQ 各有什么优缺点?

四种都能做消息中间件,但底层模型不同,模型决定了上限。Kafka 是分布式日志,吞吐高、适合事件流和大数据接入;RabbitMQ 是智能代理,路由灵活、适合复杂工作流;RocketMQ 面向阿里系业务,事务消息和延时消息好用;ActiveMQ 是经典 JMS,存量多、新项目较少作为首选。选型看吞吐、顺序、事务、运维成本和团队经验,而不是看谁更时髦。

三个容易踩的坑

  • 模型决定上限:日志型拉消费适合海量,代理型推消费适合多协议和复杂路由,选错模型后面怎么调都别扭。
  • 生态绑定真实成本:和 Spring Cloud Alibaba 或大数据栈绑定会减少对接时间,别只比功能点。
  • 运维比功能更重要:监控、扩缩容和多租户做不好,功能再全也会在高峰翻车。

什么时候用:技术选型评审时用这张对比表提问;已经选定的项目不要无故替换。先写清峰值 TPS、消息大小和是否要事务,再定产品。


二、高可用:broker 挂了生产和消费怎么继续?

消息队列高可用指 broker 挂掉后生产消费仍能继续,数据还在。手段是多副本、故障转移、跨机房复制和客户端重试。Kafka 用 ISR 和 controller,RocketMQ 用主从或 DLedger,RabbitMQ 用镜像或仲裁队列。高可用不是把机器堆多,而是明确 RPO 和 RTO:能丢几秒数据、切换要多久。客户端还要能发现新 leader,否则只是服务端活着。

三个容易踩的坑

  • 副本数和最小同步副本:只写本地盘的单点不能叫高可用,acks 与 min ISR 要一起设。
  • 控制器或 NameServer 也要冗余:分区副本再多,元数据挂了一样不可用。
  • 演练切换:定期杀 leader 看是否丢消息、是否长时间不可写,比看监控绿点有用。

什么时候用:生产集群上线、等保和容量规划时把高可用写进架构;开发环境单机即可。变更前先看副本是否在同一机架,避免一次断电全军覆没。


三、去重:怎么保证消息不被重复消费?

消息重复消费几乎无法从中间件侧彻底消灭,因为至少一次投递和消费者重试都会重放。业务要把处理做成幂等:同一消息号执行多次结果相同。常见手段是唯一键入库、状态机版本号和去重表。缓存 setnx 只能挡短时重复,不能当唯一真相。消费端还要区分可重试异常和不可重试异常,避免毒消息把分区卡住。本文只讨论消费侧去重,不谈 broker 内部去重。

三个容易踩的坑

  • 幂等键要业务化:用订单号加事件类型,不要只用 broker 的 offset,重平衡和换组会变。
  • 写库与去重同一事务:先插去重表再改业务,或用唯一约束吃掉重复插入。
  • 重试有上限和死信:超过次数进死信人工处理,不要无限重试把下游打爆。

什么时候用:所有写操作的消费者都要按幂等设计;只读通知可以宽松。联调时主动重放同一条消息,验证不会加两次钱或发两次券。


四、可靠:怎么保证消息在链路上不丢?

可靠性传输要保证消息在生产、落盘、复制和消费确认这条链上不丢。生产者要等 broker 确认,broker 要写磁盘并复制到副本,消费者处理成功后再提交位移。任何一环异步确认都可能丢。至少一次投递是常见默认,恰好一次要靠幂等和事务。网络闪断时重试会产生重复,所以可靠和去重往往一起做。不同产品的 ack、ISR 和事务语义名字不同,但账要这样算。

三个容易踩的坑

  • 生产端确认级别acks=all 或同步刷盘才能在 leader 宕机时不丢,异步发送必须处理回调失败。
  • 消费端先处理再提交:先提交位移再处理会在崩溃时丢消息。
  • 事务和幂等互补:broker 事务防生产重复,业务幂等防消费重复,只开一边不够。

什么时候用:资金、库存和关键通知必须把可靠性协议写进方案;日志和指标可以丢一点。上线前做杀进程和杀磁盘演练,确认不会静默丢消息。


五、顺序:怎么保证消息的顺序性?

消息顺序性指同一业务实体的事件要按发生次序被处理,例如先创建订单再支付。全局顺序极贵,通常只保证分区内或单队列内有序。Kafka 把同一 key 发到同一分区,RocketMQ 用顺序消息和单线程消费。只要消费者并发或失败重试插队,顺序就会破。设计时先问清楚要的是全局有序、分区有序还是最终一致,不要默认 MQ 会帮你排好队。

三个容易踩的坑

  • 分区键就是顺序边界:订单号作 key 才能保证同一订单有序,用随机 key 等于放弃顺序。
  • 单线程消费换吞吐:有序分区往往要单消费者,热点订单会成为瓶颈,可按实体分片。
  • 重试不能插队:失败消息要阻塞后续或进独立重试队列并带版本号,避免旧事件覆盖新状态。

什么时候用:账户余额、状态机和流水类业务需要顺序;日志收集和通知类通常不需要。评审时写出顺序的粒度和失败策略,再选产品功能。


六、延时与失效:积压和过期消息怎么处理?

消息积压和过期失效是两件相关的事。生产者突然变快或消费者挂了,队列里的消息会越堆越久;如果 broker 设了 TTL,堆久的消息会被丢掉,业务就像消息从未发生。延时还可能来自慢消费、重平衡、大消息和磁盘 IO。处理思路是先保证消费者能力跟上,再用死信和监控兜底,而不是把 TTL 设得很短假装系统很干净。延时队列则是另一种有意为之的定时投递,不要和故障导致的延时混为一谈。

三个容易踩的坑

  • 积压要分层看:分区级、消费者组和单条处理时延,定位是扩容还是修好卡死的消费逻辑。
  • TTL 会丢业务:过期前要有死信或落库补偿,支付和订单类消息尤其不能只靠过期删除。
  • 延时投递用专门机制:时间轮、延时队列或定时表,不要让消费者 sleep 冒充延时。

什么时候用:出现消费延迟告警、消息莫名消失或要做超时关单时看这套问题;日常吞吐正常不必调 TTL。先把监控和重试策略补齐,再谈扩分区。


七、设计:自建或二次封装 MQ 时系统怎么拆?

设计消息队列系统要同时考虑生产、存储、投递和运维。主题怎么分区、副本怎么放、消费位移存在哪、积压怎么观察,这些比选一个开源产品名字更关键。自研或二次封装时,要把顺序、重复、丢失和积压当成明确的 SLA。存储可以是磁盘日志或数据库表,投递可以是推或拉。管理面还要有死信、重试和权限。本文把消息系统设计拆成主题模型、持久化与消费模型三块来看。

三个容易踩的坑

  • 日志型分区是扩展单元:按业务键哈希到分区,扩容是加分区而不是加无界队列。
  • 位移是消费者的进度:提交太早会丢,太晚会重复,至少一次投递要配幂等。
  • 积压是一等指标:没有消费延迟和死信处理,上线后只能靠重启硬扛。

什么时候用:评估要不要自建 MQ、写技术方案或对比云产品时按这套清单提问;日常业务优先用成熟中间件。设计评审时先写清丢失和重复的容忍度,再画架构图。


附:各 MQ 特性对比表

产品 模型 / 定位 擅长场景 高可用机制 备注
Kafka 分布式日志,拉消费 事件流、大数据接入、海量吞吐 ISR + Controller,依赖 min ISR 与 acks 吞吐与扩展性上限高,运维复杂度也高
RabbitMQ 智能代理,推消费,路由灵活 复杂工作流、多协议路由 镜像队列 / 仲裁队列 路由能力强,超大规模吞吐不如 Kafka
RocketMQ 阿里系业务模型,事务/延时消息友好 电商交易、事务消息、延时关单 主从 / DLedger 事务消息与延时消息开箱好用
ActiveMQ 经典 JMS 实现 存量系统、传统企业集成 主从 / 网络集群 新项目较少作为首选

选型铁律:先写清峰值 TPS、消息大小、是否要事务与顺序,再定产品;模型决定上限,运维决定能不能扛住高峰。