消息队列的题很少考 API,更多考「你敢不敢把它放进核心链路」。回答时先讲它带来的三个收益,再讲它引入的四个新问题:一致性、重复、顺序和运维复杂度。
1. 为什么要用消息队列?引入了哪些问题?
三个典型收益:解耦(生产者不需要知道有多少下游)、削峰(把突发流量缓冲到消费端能承受的速率)、异步(把非关键步骤移出主链路,缩短响应时间)。
代价同样明显:系统从「一次调用」变成「最终一致」,需要处理消息丢失、重复、乱序和积压;链路变长后,排查问题需要消息 id 贯穿;中间件本身也需要高可用、监控和容量规划。
所以不是所有调用都适合异步。强一致、需要立即返回结果、调用方必须知道成败的场景,同步调用更简单。用 MQ 之前先问:业务能否接受延迟和最终一致?
一个常见反模式是「为了解耦而拆出十几个消费者」,结果每次改消息格式都要协调所有下游,耦合只是从代码转移到了契约。
2. RabbitMQ、RocketMQ、Kafka 怎么选?
RabbitMQ 基于 Erlang 和 AMQP,路由能力最强,支持 direct、topic、fanout、headers 等交换机模型,单条消息确认和灵活的重试、死信机制成熟,适合业务消息和中小规模异步。
RocketMQ 是 Java 生态,吞吐高,原生支持顺序消息、事务消息、延迟消息和死信队列,比较贴合电商、订单这类业务场景;运维和生态相比 Kafka 稍窄。
Kafka 以分区日志为核心,吞吐最高,适合日志、埋点、流处理和数据管道;它按分区保证顺序,消费位点由客户端管理,缺少原生的单条重试和死信,业务语义要自己补。
选型要看「消息形态」:要复杂路由和逐条确认,选 RabbitMQ;要事务消息和业务顺序,选 RocketMQ;要大吞吐的流式数据,选 Kafka。三者都能做到百万级,不要只拿基准测试的峰值做决定。
3. 如何保证消息不丢失?
要分三段看:生产、存储、消费。
生产端:RabbitMQ 用 publisher confirm 加持久化交换机/队列;RocketMQ 用同步发送并检查 SendStatus,或事务消息;Kafka 设置 acks=all,配合 min.insync.replicas 和重试。异步发送必须处理回调失败,不能发出去就不管。
存储端:消息和队列都要持久化,副本数至少 2 到 3,并确认刷盘策略。同步刷盘更安全但更慢,异步刷盘在宕机时可能丢最近一批。
消费端:处理成功后再确认。RabbitMQ 用 basicAck,RocketMQ 返回 CONSUME_SUCCESS,Kafka 在业务完成后提交 offset。先确认后处理,进程崩溃就会丢消息。
即便如此,也只能做到「尽量不丢」,所以关键业务还需要对账和补偿。
4. 如何保证消息不被重复消费?
先明确:端到端严格一次很难做到。生产端重试、网络超时、消费者在「处理成功但确认失败」时重投,都会产生重复,工程上普遍采用「至少一次投递 + 消费端幂等」。
幂等的常见实现:业务唯一键加数据库唯一索引;去重表记录消息 id;Redis SET NX 做短期去重;状态机只允许特定状态迁移;乐观锁版本号。选择哪种取决于业务,金额类操作更适合唯一约束和流水表。
Kafka 的事务和幂等生产者能保证「Kafka 内部」的写入不重复,但跨数据库的副作用仍要自己做幂等。RocketMQ 的事务消息解决的是「本地事务与发消息」的原子性,不是消费去重。
面试时最好补一句:去重表要设置合理的保留期,并考虑并发下先插入还是先处理。
5. 顺序消息怎么实现?
顺序的前提是「同一业务键的消息进入同一队列或分区,并且只被一个消费者按序处理」。
Kafka 用消息 key 做分区,同一 key 落到同一 partition,消费者组内一个分区只分配给一个消费者;RocketMQ 用 MessageQueueSelector 选择队列,消费端用顺序监听器。
破坏顺序的常见原因:生产端重试后消息落到其他分区;消费端多线程并行处理;失败重试把消息挪到队尾;扩容导致分区重新分配。所以顺序消息通常要配合「失败阻塞重试」或把失败消息转入专门的重试队列,避免影响后续消息。
还要评估是否真的需要全局顺序。绝大多数场景只需要「同一订单/同一用户」的局部顺序,全局单分区会牺牲吞吐。
6. 消息积压了怎么办?
先判断原因:消费者挂了、消费变慢(下游数据库或接口)、生产突增、分区数不足,还是消息体过大。
短期止损可以扩容消费者,但 Kafka 的并行度受分区数限制,RocketMQ 受队列数限制,扩容前先确认。必要时临时把积压消息转投到一个分区更多的新 topic,用更多消费者快速消化,再切回正常链路。
消费端可以做批量拉取、批量写库、减少同步远程调用;非核心消息可以降级或延后处理。同时要防止「为了追平而无限并发」把数据库打垮。
长期要做容量评估和监控:Kafka 看 consumer lag,RocketMQ 看堆积量,RabbitMQ 看队列深度。积压告警要带速率和预计消化时间,而不只是一个绝对数字。
7. 事务消息和本地消息表怎么选?
RocketMQ 事务消息的流程是:先发半消息,执行本地事务,再根据结果提交或回滚;如果 Broker 长时间收不到确认,会回查生产者的本地事务状态。
它解决的是「本地事务成功但消息没发出去」或「消息发出但本地事务回滚」的问题,适合已有 RocketMQ 且业务链路相对简单的场景。
本地消息表把待发消息和业务数据放在同一个数据库事务里,再由定时任务或 CDC 投递。它不依赖 MQ 的事务能力,跨 MQ 也能用,代价是要多一张表、需要重试和清理,并且投递是异步的。
两者都只能保证最终一致。选择时看团队对中间件的掌握程度、是否已有可靠的调度和对账体系。不要让「事务消息」变成「分布式强一致」的错觉。
8. 死信、延迟消息和重试应该怎么设计?
重试要分级:瞬时故障(网络抖动、下游限流)适合指数退避加抖动;业务性失败(参数错误、库存不足)不应无限重试,直接进死信并告警。
延迟消息适合「超时未支付关单」「定时提醒」这类场景。RabbitMQ 可以用 TTL 加死信交换机,或延迟插件;RocketMQ 支持延迟级别和 5.0 的定时消息;Kafka 没有原生延迟,需要外部调度或时间轮。
死信队列不是垃圾桶:要能查询、告警、人工重放,并保留原始消息和失败原因。重放接口要做权限和幂等保护,避免把故障放大。
还要防「毒消息」:某条消息必然处理失败时,如果一直重试会阻塞整个队列。顺序队列尤其要设置最大重试次数,超过后转死信,保证后续消息能继续。
易错点与自测
- 「消息不丢」是三段式问题,只答 Broker 持久化不完整。
- 「不重复消费」本质是消费端幂等,别承诺端到端 exactly-once。
- 顺序、积压、重试三者互相影响,扩容分区可能破坏顺序。
自测:① 画出 RocketMQ 事务消息的半消息与回查流程;② 说明 Kafka 中 acks=all 仍可能丢消息的条件;③ 给出一个「消费成功但确认失败」的幂等方案。
参考资料
- Apache Kafka 官方文档 — 分区、副本、acks 与事务语义
- Apache RocketMQ 官方文档 — 顺序消息、事务消息、延迟消息与重试
- RabbitMQ 官方文档:可靠性指南 — 确认、持久化与死信