面试复习资料 列表全部文章
面试复习资料

面试复习:消息队列 Kafka / RocketMQ / RabbitMQ(可靠性、幂等、顺序、事务消息、积压,初中高级三层)

按初级/中级/高级三层整理消息队列面试:三款 MQ 选型对比、投递可靠性三段保证、重复消费与幂等、顺序消息、事务消息、积压与延迟消息处理、Kafka 高吞吐原理与 RocketMQ 存储模型,附追问链与速查表。

更多

几乎每个后端都会在简历里写一句「使用 MQ 实现异步解耦、削峰填谷」,写的时候轻描淡写,面试的时候就会被“三连”问懵:消息会不会丢?会不会重复?能不能保证顺序?这三个问题是消息队列的“铁三角”,再往下追就是事务消息、积压、延迟消息、选型,最后落到一句:“你们线上出过 MQ 事故吗?”

我自己的经验是:MQ 这一章别按产品背,按“一条消息的一生”来背——它从生产者出发,经过 Broker 存储与复制,到消费者处理并确认,每一段都可能丢、可能重、可能乱。把这三段各自的故障点和对策讲清楚,选型与产品原理不过是在给这条主线加注脚。

本文根据公开面经与官方文档整理改编,不是我本人某场面试的实录。按难度分三层:🌱 初级(0–3 年)是什么、怎么用;🌿 中级(3–5 年)为什么、怎么选、怎么排查;🌳 高级(5 年+)架构取舍、线上治理、量化复盘。每层给「标准回答要点 / 追问链 / 加分项」。关于版本,我在文中标注了 Kafka 与 RocketMQ 4.x/5.x 的差异,凡是我没有在官方文档里核对过的默认值,都写明“以你使用版本的文档为准”。

一、知识地图:一条消息的一生

下图把可靠性三段、以及每一段常见的“丢/重/乱”风险画在一起,后面所有专题都能挂在这张图上。

① 生产者端 风险:发送失败没重试、异步发送不看结果 对策:同步发送 / 回调 + 重试 Kafka acks=all、开幂等生产者 RabbitMQ publisher confirm 业务库 + 本地消息表兜底 ② Broker 端 风险:内存未刷盘宕机、单副本损坏 对策:刷盘策略 + 多副本 Kafka:replication.factor=3 min.insync.replicas=2,禁用脏选举 RocketMQ 同步刷盘 / 主从同步 ③ 消费者端 风险:先提交位点后处理、处理异常吞掉 对策:处理成功后再确认 失败重试 + 死信队列 至少一次投递 → 必然重复 所以消费端必须幂等 横向的三个铁三角问题 丢:三段任意一段没保证;重复:至少一次 + 重试 + 重平衡;乱:多分区并行 / 重试 / 多线程消费 进阶专题:事务消息(本地事务与发消息的原子性)· 顺序消息 · 延迟消息 · 积压治理 产品原理:Kafka 顺序写 + 零拷贝 + 批量;RocketMQ CommitLog + ConsumeQueue;RabbitMQ Exchange 路由 结论:没有“不丢不重不乱”的银弹,工程上用“至少一次 + 幂等 + 按 key 有序”组合

二、选型:Kafka、RocketMQ、RabbitMQ 到底怎么选

🌱 初级:先说清 MQ 解决什么问题

标准回答要点:消息队列的三大作用——异步(下单后发短信、加积分不必同步等)、解耦(订单服务不用直接依赖库存、营销、通知服务,只发事件)、削峰(秒杀时把瞬时流量缓冲到队列,下游按自己的能力消费)。代价也要会说:系统复杂度上升(要处理丢、重、乱、积压)、一致性变弱(从强一致变成最终一致)、多了一个需要高可用的组件。

基本概念:Producer / Consumer / Broker / Topic;Kafka 的 Partition 与 Consumer Group;RocketMQ 的 MessageQueue(官方概念文档中称为 Topic 下最小的存储单元)与 Consumer Group;RabbitMQ 的 Exchange / Binding / Queue。

🌿 中级:三款产品的定位差异

维度KafkaRocketMQRabbitMQ
定位分布式日志 / 事件流平台,大数据、日志、埋点管道业务消息中间件,电商、金融类事务型场景企业消息总线,复杂路由、低延迟小规模
吞吐极高(单机可到百万级消息/秒量级,取决于消息大小与配置)高(十万级/秒常见)中(万级到十万级,取决于队列类型与持久化)
顺序分区内有序队列内有序,官方提供 FIFO 消息类型单队列单消费者有序,多消费者会乱
事务消息有事务(偏 Kafka 内部读-处理-写的原子性)有事务消息(半消息 + 回查),面向“本地事务 + 发消息”无原生分布式事务消息,用 confirm + 本地消息表
延迟消息无原生,需要自己实现(多级 topic / 时间轮)原生支持(4.x 固定级别,5.x 可指定时间,上限以版本文档为准)TTL + 死信交换机,或延迟消息插件
消息回溯按 offset 或时间重新消费,天然支持按时间回溯消费位点消费即删除,回溯要借助 Streams
路由/过滤按 topic/分区,过滤能力弱Tag、SQL92 属性过滤direct / fanout / topic / headers 交换机,路由最灵活
Topic 数量分区多了会影响性能与恢复时间大量 Topic 表现更好(所有 Topic 共用一个 CommitLog)队列数量大也要注意资源
运维4.0 起去掉 ZooKeeper,使用 KRaftNameServer + Broker,5.x 引入 Controller 模式Erlang 环境,集群与镜像/仲裁队列

我引用 JavaGuide 那篇 2026 年更新的面试总结里的几个观点,与我的经验吻合:Kafka 强调顺序写、页缓存、零拷贝带来的吞吐,天然适合事件流和日志回放;RocketMQ 面向业务消息,事务消息、延迟消息、精细顺序保障是它的卖点,存储采用 CommitLog + ConsumeQueue;RabbitMQ 靠 Exchange 做灵活路由,并且现代版本推荐用 Quorum Queue 与 Streams 取代旧的经典队列。

怎么答选型(模板):先问场景,再谈产品。“我们做的是订单支付这类业务消息,要事务消息与延迟消息,量级中等,所以选 RocketMQ;行为日志、埋点、CDC 这类大吞吐流式数据用 Kafka,并且下游有 Flink/Spark 消费;后台任务、邮件通知这类低吞吐、复杂路由可以用 RabbitMQ。”并补一句:选型还要看团队运维能力和云厂商托管服务,不要为了性能数字选一个没人会运维的组件。

🌳 高级:选型背后的取舍与迁移

  • 吞吐 vs 延迟 vs 一致性:Kafka 靠批量与顺序 IO 换吞吐,单条消息端到端延迟因批量(linger.ms)略高;RabbitMQ 单条延迟低但吞吐上限低;RocketMQ 居中且业务特性丰富。没有最优,只有匹配。
  • 多租户与治理:业务线多时,要有统一的 Topic 申请、命名、配额(生产/消费限流)、ACL、消息大小上限(比如 1MB)、保留时间标准;否则一个业务的大消息或死循环重试就能拖垮共享集群。
  • 迁移与双写:从 A 迁到 B,通常做双写 + 双消费校验 + 灰度切消费者 + 保留回退窗口;消息 ID、顺序性、事务语义在两个产品之间不等价,要逐项映射。
  • “为什么不直接用数据库表当队列”:量小时可以(本地消息表就是这个思路),但吞吐、消费者扩展、堆积能力与运维成熟度不如专用 MQ;这个问题的成熟回答是“规模小、强一致要求高时,我确实会用表 + 轮询,规模上来再演进”。

三、消息可靠性:三段保证

🌱 初级:消息会不会丢?在哪丢?

标准回答要点:会,丢的位置有三个:生产者 → Broker、Broker 自身存储、Broker → 消费者。所以可靠性要三段一起做,缺一段都是白做。

  • 生产者:不要“发了就不管”。用同步发送或带回调的异步发送,拿到 Broker 的确认再认为成功,失败要重试;仍失败要落库(本地消息表)后台补偿。
  • Broker:消息要落盘并复制到多个副本才算安全。
  • 消费者:先处理业务,成功后再确认(ack / 提交位点);不要自动确认后异步处理,也不要在 catch 里吞掉异常。

三种投递语义:至多一次(可能丢,不重复)、至少一次(不丢,可能重复)、恰好一次(不丢不重,代价最大,通常是“至少一次 + 幂等”的等效实现)。实际业务普遍选至少一次 + 消费端幂等。

🌿 中级:每个产品的关键参数

Kafka:

  • 生产端:acks=all(等待 ISR 中所有副本确认;注意是 ISR 而不是所有副本);retries 足够大;开启幂等生产者 enable.idempotence=true(Kafka 3.0 起客户端默认开启,为每个生产者分配 PID、每个分区维护序列号,Broker 据此去重);发送用带回调的 send(record, callback),不要只 fire-and-forget。
  • Broker 端:replication.factor=3;min.insync.replicas=2(与 acks=all 配合,ISR 少于 2 时写入直接失败而不是降级丢数据);unclean.leader.election.enable=false(禁止让不在 ISR 里的副本当 leader,否则可能丢已确认的数据);刷盘靠副本与页缓存,一般不强制 fsync(flush.messages 等保持默认),可靠性由多副本承担。
  • 消费端:enable.auto.commit=false,业务处理成功后手动提交(commitSync 或批量提交);提交位点的时机决定“丢”还是“重”:先提交后处理可能丢,先处理后提交可能重。

JavaGuide 的总结里有一个很值得记的点:“acks=all 等的是 ISR 确认,不是所有副本”,所以必须和 min.insync.replicas 配套使用,否则 ISR 只剩一个 leader 时,acks=all 也退化成 acks=1。

RocketMQ:

  • 生产端:使用同步发送(send)并检查 SendStatus;异步发送必须在回调里处理失败;重试次数、超时按业务配置。
  • Broker 端:flushDiskType=SYNC_FLUSH(同步刷盘,收到消息刷盘后才返回,最可靠也更慢)与默认的异步刷盘的取舍;主从之间 SYNC_MASTER(同步复制)与 ASYNC_MASTER;5.x 与 DLedger / Controller 模式提供基于 Raft 的自动选主。
  • 消费端:并发消费返回 CONSUME_SUCCESS 才算成功,返回 RECONSUME_LATER 或抛异常会进入重试队列;重试到上限后进入死信队列,需要人工或补偿程序处理。

RabbitMQ:

  • 生产端:开启 publisher confirm,Broker 落地(持久化队列的话是落盘后)才确认;无法路由到队列的消息用 mandatory + return 回调发现。
  • Broker 端:交换机、队列、消息都持久化;关键业务用仲裁队列(Quorum Queue),基于 Raft 复制,官方文档明确它适合“不能丢消息”的长生命周期队列(如订单);它还带有投递次数限制处理毒药消息(x-delivery-count,默认上限 20 次,超过后丢弃或死信),并可配置 at-least-once 死信策略。
  • 消费端:手动 ack,业务成功再 ack;处理失败 nack 时谨慎设置 requeue,无限重投会形成“毒药消息死循环”;合理设置 prefetch(QoS)避免单个消费者囤积大量未确认消息。

追问链:

  1. “如果 Broker 已经存了,但确认包丢了,生产者以为失败重发了怎么办?”——这就是重复的来源之一,所以要靠幂等生产者(Kafka)或消费端幂等兜底。
  2. “同步刷盘一定更好吗?”——不一定。同步刷盘每条消息(或每批)都等 fsync,吞吐显著下降;多副本同步复制往往能提供不弱于单机刷盘的可靠性,所以很多场景选“异步刷盘 + 同步复制”,具体看丢消息容忍度与吞吐要求。
  3. “消费者宕机,消息会丢吗?”——不会,因为没 ack(没提交位点),重启或重平衡后会由其它消费者从上次提交位点继续,代价是可能重复。

🌳 高级:端到端“不丢”的工程闭环

  • 发送侧的终极兜底:本地消息表(事务外发,Outbox)。业务数据与“待发送消息记录”在同一个本地事务里写入;后台任务(或 CDC 订阅表的 binlog)扫描待发送记录并投递,收到 Broker 确认后标记已发送,失败自动重试。这样即使 MQ 整体不可用,消息也不会丢,只是延迟。这比“先提交事务再发消息”(提交后宕机会丢)和“先发消息再提交事务”(事务回滚会产生幽灵消息)都可靠。
  • 对账兜底:再可靠的链路也应该有离线对账——每天按业务单据核对“订单已支付”与“下游已收到”,发现缺失补发;把它当作“最后一道保险”,并把对账差异率作为监控指标。
  • 消息可追踪:每条消息带业务 key 与 traceId,Broker 查询(RocketMQ 支持按 Key 查询,依赖 IndexFile)、日志平台可联查,出问题时能回答“这条消息在哪一段消失了”。
  • SLA 量化:给出“99.99% 消息 5 秒内到达、对账差异率低于十万分之一”这类目标,并说明用哪些指标(生产成功率、端到端延迟分位、消费 lag、死信数量)来衡量。

加分项:能说清 Kafka 的“恰好一次”(幂等生产者 + 事务 + isolation.level=read_committed)只保证 Kafka 内部“读-处理-写”闭环,一旦消费后写外部数据库或调外部接口,仍要靠业务幂等;这是官方语义边界,不要在面试里说成“Kafka 能做到端到端恰好一次”。

四、重复消费与幂等

🌱 初级:为什么会重复,怎么处理

标准回答要点:重复的来源有:①生产者重试(确认丢失);②消费者处理成功但确认失败或宕机,重新投递;③重平衡(Rebalance)导致位点回退;④人工回溯位点或重放。所以“至少一次”天然会重复,处理重复的责任在消费端——让同一条消息处理一次和处理多次结果一致,这就是幂等。

常见幂等手段:

  • 唯一索引 / 去重表:以业务唯一键(订单号、支付流水号)建唯一索引,重复插入报主键冲突就忽略。最稳,依赖数据库。
  • 状态机:订单状态只允许 待支付 → 已支付,用 update ... set status=2 where id=? and status=1,影响行数为 0 就说明已处理。
  • Redis 去重:SET msgKey 1 NX EX 86400,成功才处理。注意它是“先占位再处理”,占位后处理失败要清除占位,否则消息重试会被误判为已处理;要处理“处理中”状态。
  • 天然幂等操作:set x = 5 是幂等的,x = x + 5 不是,尽量把累加改成基于版本的覆盖。

🌿 中级:幂等 key 怎么选,坑在哪

  • 不要只依赖 MQ 的 messageId:生产者重发同一业务事件时 messageId 会变化(重试可能生成新的消息),JavaGuide 的总结也强调 messageId 只在传输层有用,真正的幂等要用业务唯一键(订单号 + 事件类型)。
  • 幂等要和业务写入在同一个本地事务里:“先查去重表没有,再处理,再写去重表”是三步,并发下有竞态;正确做法是去重表插入 + 业务更新放在同一事务,靠唯一索引挡住并发。
  • 去重记录的保留时间:重复窗口有多长,去重记录就要保留多久(比如覆盖消息最大保留期与重试窗口),过短会放过重复,过长占存储;可以分层:Redis 存近期、DB 唯一键做最终裁判。
  • 幂等不等于过滤旧消息:如果消息乱序,旧状态的消息晚到,幂等 key 没冲突却会把新状态覆盖回旧状态,需要版本号 / 时间戳比较(只接受版本更大的更新)。
  • 幂等的边界:涉及外部系统(发短信、调支付渠道)时,要把幂等键透传给下游(如渠道的商户订单号),依赖下游的幂等接口;否则再怎么去重也拦不住调用两次。
// 消费端幂等示例:去重表 + 业务更新同一事务(示意)
@Transactional(rollbackFor = Exception.class)
public void onPaid(PayEvent e) {
    try {
        // uk_biz_key(biz_key) 唯一索引:订单号 + 事件类型
        consumeLogMapper.insert(new ConsumeLog(e.getOrderNo() + ":PAID"));
    } catch (DuplicateKeyException dup) {
        return;                       // 已处理过,直接确认消息
    }
    // 状态机保护:只允许待支付 → 已支付
    int rows = orderMapper.markPaid(e.getOrderNo());   // update ... where status = 1
    if (rows == 0) {
        log.warn("订单状态不符,忽略 {}", e.getOrderNo());
    }
}

🌳 高级:幂等的体系化

  • 在框架层统一做:把幂等抽成消费模板或注解(业务 key 表达式、去重存储、过期时间、失败清理),避免每个团队自己写、写出各种竞态。
  • “处理中”的第三态:长耗时处理的消息,Redis 占位过期早于处理完成会导致另一个消费者重复进入;用“处理中(带心跳续期)/ 已完成”两态,并对“处理中”的消息延迟重试而不是直接丢弃。
  • 幂等与重试配合:重试策略(指数退避、最大次数、死信)必须假设“每次重试都可能是重复”;死信里的消息人工重放同样要走幂等。
  • 用数据说话:统计重复率(去重命中数 / 消费数)。重复率突然升高往往是重平衡频繁、消费超时被重投或 Broker 抖动的信号,是很好的告警指标。

五、顺序消息

🌱 初级:什么是顺序消息,怎么保证

标准回答要点:业务上“同一个订单的创建、支付、发货必须按顺序处理”,并不要求全局有序,只要求同一业务 key 有序。做法:发送时按业务 key 选择固定的分区(队列),Kafka 用 key 的哈希决定分区,同一分区内消息有序;RocketMQ 用 MessageQueueSelector 按订单号取模选择队列。消费时,一个分区/队列同一时刻只能由一个消费者线程串行处理。

全局有序只能用单分区 + 单消费者,吞吐降到单点上限,所以能不做就不做。

🌿 中级:顺序被破坏的五个位置

  1. 生产者重试导致乱序:Kafka 里消息 1 发送失败、消息 2 成功、消息 1 重试成功,顺序变成 2、1。解法:开启幂等生产者(序列号保证),此时 max.in.flight.requests.per.connection 只要不超过 5 仍能保证分区内有序;不开幂等的话要把它设为 1(牺牲吞吐)。
  2. 分区数变化:扩分区后 hash(key) % 分区数 变了,同一个 key 会漂移到新分区,新老分区的消息消费进度不同,顺序会被打破。要么一开始预留足够分区,要么扩容时有迁移与停写窗口。
  3. 消费者多线程并发处理:拿到一个分区的消息后丢进线程池并行处理,顺序就乱了。解法:按业务 key 哈希到固定的内部单线程队列(“分区内再分片串行”),保持同 key 串行、不同 key 并行。
  4. 消费失败重试:RocketMQ 顺序消费(MessageListenerOrderly)失败时会阻塞该队列并重试(默认无限次,需要设置上限或告警),否则一条毒药消息会堵死整个队列,这是顺序性的代价;并发消费失败则进入重试队列,天然打乱顺序。
  5. Rebalance:重平衡时分区归属变化,处理中的消息可能被新消费者重复消费,要配合幂等。RocketMQ 的顺序消费者会在 Broker 端对队列加锁,保证同一时间只有一个消费者处理某个队列(但锁在重平衡瞬间仍存在短暂窗口,要有幂等兜底)。

RabbitMQ:单队列 + 单消费者可保证有序;多个消费者并发处理会乱;消息重新入队(requeue)会打乱顺序;如需分区顺序,需要自己用多个队列 + 一致性哈希交换机(Consistent Hash Exchange 插件)把同 key 路由到同一队列。

追问链:“顺序消息 + 高并发怎么兼顾?”——同 key 串行、不同 key 并行;分区数取得足够大(如 32、64);每个分区消费者处理要快,避免热点 key 把某个分区堵死。“热点 key(一个超级大商家订单)导致某分区积压怎么办?”——这就是顺序性的代价,可以对热点 key 做二级拆分(例如“商家 id + 订单号哈希后缀”),只保证更细粒度的顺序,前提是业务允许。

🌳 高级:顺序性与可用性的取舍

  • 顺序消息本质是用可用性和吞吐换局部顺序:一个分区不可用,它上面所有 key 的消息都暂停;一条毒药消息会阻塞后面所有消息。设计上要有毒药消息隔离:重试 N 次后转入死信并放行后续消息,同时通过版本号让业务能容忍缺一条(例如状态机下一条消息带全量状态)。
  • 能不用顺序就不用:更好的设计是让消息携带全量状态与版本号(“订单当前状态 = 已发货,版本 7”),消费者用版本比较丢弃旧消息,容忍乱序,吞吐和可用性都更好。面试里说出“我会先想办法让消费端容忍乱序,实在不行才上顺序消息”,是高级岗位很加分的表述。

六、事务消息

🌿 中级:RocketMQ 事务消息怎么工作

要解决的问题:本地事务(写订单)与发消息(通知库存)如何做到“要么都成功要么都失败”。先提交事务再发消息,提交后宕机消息丢;先发消息再提交事务,事务回滚会有幽灵消息。

RocketMQ 事务消息流程(对照官方“事务消息”文档):

  1. 生产者发送半消息(Half Message)到 Broker,Broker 存下来,但对消费者不可见;
  2. Broker 返回发送成功,生产者执行本地事务;
  3. 本地事务结束后,生产者向 Broker 发送二次确认:Commit(消息变为可消费)或 Rollback(Broker 丢弃半消息);
  4. 如果 Broker 一直没收到二次确认(网络问题、生产者宕机重启),Broker 会主动发起回查(check-back),询问生产者集群中的任一实例这条半消息对应本地事务的状态,生产者根据本地数据(如订单是否存在)返回 Commit / Rollback / Unknown;
  5. 超过最大回查次数仍无法确定,Broker 会按 Rollback 处理(官方文档:无法确认时超过一定时间后自动回滚;具体回查间隔与最大次数请以所用版本文档和配置为准)。
// RocketMQ 事务消息:本地事务与回查逻辑(示意,API 以所用版本为准)
public class OrderTxListener implements TransactionListener {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            orderService.createOrder((OrderDTO) arg);      // 本地事务:写订单
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查:根据业务 key 查订单是否已落库,必须是幂等只读查询
        boolean exists = orderService.existsByOrderNo(msg.getKeys());
        return exists ? LocalTransactionState.COMMIT_MESSAGE
                      : LocalTransactionState.ROLLBACK_MESSAGE;
    }
}

要牢记的边界(官方文档与面经共识):

  • 事务消息保证的是最终一致,不是强一致;它只保证“本地事务与消息发出”的原子性,不保证消费者一定处理成功,消费端仍要重试与幂等;
  • 5.x 中事务消息要求 Topic 的消息类型为 Transaction(官方概念文档:一个 Topic 只允许一种消息类型);
  • 回查逻辑必须可重入、只读、快速,并且本地库里要有能判断事务结果的依据(订单表本身或事务日志表)。

Kafka 的事务与之不同:它解决的是“消费 topic A → 处理 → 写 topic B + 提交位点”在 Kafka 内的原子性(transactional.id、initTransactions / beginTransaction / commitTransaction,消费者用 isolation.level=read_committed 只读已提交消息)。它不是“数据库本地事务 + 发消息”的分布式事务。面试时要主动区分这两种“事务”,很多人栽在这里。

RabbitMQ:AMQP 有 tx 事务模式(性能很差,很少用);实践里用发布确认 + 本地消息表。

🌳 高级:事务消息 vs 本地消息表 vs Seata

方案优点缺点 / 适用
RocketMQ 事务消息业务侵入小,回查由 Broker 驱动,无需自建扫表绑定 RocketMQ;要写回查接口;仅最终一致
本地消息表(Outbox)与 MQ 无关,通用;可用 CDC 订阅表变更要自己扫表 / 订阅,多一张表与运维;有轻微延迟
Seata AT / TCC接近强一致体验、事务语义完整性能损耗、全局锁与运维复杂度,TCC 要写三个接口

成熟的回答是:大多数场景选“本地事务 + 可靠消息 + 幂等 + 补偿”,用事务消息或 Outbox 二选一;只有资金类强一致场景才考虑 Seata,并评估性能。复盘时要能给量化:“接入事务消息后,下单接口不再依赖库存服务的同步调用,P99 从 380ms 降到 90ms,消息丢失率通过对账为 0;代价是新增回查接口并把库存扣减改为异步,库存展示有秒级延迟。”

七、消息积压、延迟消息与死信

🌱 初级 + 🌿 中级:积压怎么处理

积压(lag)指消费速度低于生产速度,未消费消息越堆越多。排查思路要先定位原因再动手(JavaGuide 的总结也强调先区分原因):

  1. 是不是生产突增:秒杀、批量导入、上游重放。是流量问题,需要扩容或限流;
  2. 是不是消费变慢:单条处理耗时变长——慢 SQL、下游接口超时、锁竞争、Full GC、日志同步刷盘;看消费者线程栈与处理耗时监控;
  3. 是不是消费者数量/分区不够:Kafka 一个分区同一消费组内只能被一个消费者消费,消费者数量超过分区数是浪费的,加机器没用;
  4. 是不是被毒药消息卡住 / 频繁重平衡:顺序消费被阻塞;消费超时(Kafka 的 max.poll.interval.ms)导致被踢出消费组、反复重平衡,看似在消费,实则空转。

处理手段(从快到慢):

  • 先止血:限流或暂停低优先级生产者;如果积压消息价值低(日志、埋点),可以直接跳过(重置位点到最新);
  • 提升单消费者速度:批量消费、批量入库(把逐条 insert 改为批量)、线程池并行(无顺序要求时)、异步化下游、缓存热点查询;
  • 横向扩容:加消费者实例;Kafka 需要先增加分区(且只对新数据生效,且会影响 key 分布);RocketMQ 增加队列数;
  • 紧急搬运:对巨量积压,可以临时建一个分区数更多的新 Topic,写一个“搬运程序”把积压消息不经处理快速转发到新 Topic,再启动更多消费者并行消费,处理完后恢复原架构;
  • 写事后:积压期间消息可能超过保留时间被删除(Kafka 的 retention、RabbitMQ 的 TTL),要提前评估,必要时延长保留或补数据。

🌳 高级:积压的预防与量化

  • 容量模型:峰值生产 TPS × 单消息处理耗时 ÷ 单消费者并发度 = 需要的消费者并发数,再乘以 1.5–2 的冗余。例如峰值 2 万条/秒、单条处理 20ms、每消费者 8 线程并发,则理论需要 2 万 × 0.02 ÷ 8 = 50 个消费者,并保证分区数不少于消费者数。
  • 监控与告警:告警指标用“lag 的增长趋势与预计清空时间”,而不是绝对值;同时看消费 TPS、单条耗时分位数、重试与死信数量、重平衡次数。lag 绝对值大但在下降是没事的,lag 小但持续上升要提前处理。
  • 隔离与优先级:核心与非核心业务用不同 Topic / 消费组隔离,避免非核心积压拖垮核心;延时不敏感的批量任务走独立集群或低峰消费。
  • 压测与演练:定期做“消费者全停 10 分钟后恢复”的演练,验证积压后能否在承诺时间内追平,以及追平期间下游(数据库)能否扛住突然放开的消费速度——积压恢复时的“洪峰”常常把数据库打挂。

🌿 中级:延迟消息怎么做

产品做法注意
RocketMQ4.x 支持固定的延迟级别(1s、5s、10s、30s、1m…最长 2h);5.x 支持指定投递时间的定时/延迟消息(有最大延迟上限,以版本文档为准);官方消息类型里有独立的 DELAY 类型Topic 类型要匹配;不要指望精确到毫秒
RabbitMQ①消息或队列设置 TTL,过期后转入死信交换机,再由消费者监听死信队列;②延迟消息插件 rabbitmq-delayed-message-exchangeTTL 方案有队头阻塞:队列只检查队头消息是否过期,前面 TTL 长的消息会挡住后面 TTL 短的;JavaGuide 的总结也提到这个坑
Kafka无原生支持。常见做法:多个不同延迟时长的 Topic + 转发服务按时间转发;或消息里带执行时间,消费者暂停分区(pause)等待复杂度高,业务重延迟消息优先选 RocketMQ
通用方案Redis ZSet(分数为执行时间戳)+ 轮询;时间轮;数据库定时任务扫描;XXL-Job / 延迟任务框架关键场景(如“30 分钟未支付关单”)不要只依赖延迟消息,加一个兜底的定时扫描

追问:“订单 30 分钟未支付自动关闭,怎么设计?”——下单时发一条延迟 30 分钟的消息,消费者收到后先查订单当前状态,仍是待支付才关闭(消息只是触发器,状态以数据库为准,天然幂等;即使消息早到、晚到、重复到都不出错),同时加一个每分钟的兜底扫描任务处理漏网之鱼。这正是 JavaGuide 那篇总结里说的“无论消息时机如何,消费者都应核对业务状态”。

🌿 中级:重试与死信队列

  • RocketMQ:并发消费失败进入 %RETRY%消费组名 重试队列,按递增间隔重试,默认最多 16 次(4.x 常见配置,以所用版本为准),超过后进入 %DLQ%消费组名 死信队列,需要人工处理或写程序重放。顺序消费失败则原地重试阻塞。
  • Kafka:没有内置的重试与死信队列,需要业务用“重试 Topic + 死信 Topic”实现(Spring Kafka 提供 DefaultErrorHandler 与 DeadLetterPublishingRecoverer 等封装)。
  • RabbitMQ:通过死信交换机(DLX)实现;仲裁队列自带投递次数上限。注意 nack + requeue 会立刻重投,容易形成“热循环”。
  • 死信治理:死信要有告警、查看、重放工具;重放前先修复根因;重放走幂等;设置死信保留期限并定期清理。

八、Kafka 为什么吞吐这么高

🌿 中级:六个关键设计

  1. 顺序写磁盘 + 日志结构:每个分区就是一个只追加的日志,分成多个 segment 文件(默认单个 1GB,可配置),按 offset 命名;顺序写的速度可以接近内存随机写,远好于随机写磁盘。
  2. 页缓存(Page Cache):Kafka 不自己维护复杂的内存缓存,而是依赖操作系统页缓存:写入先写到页缓存,由 OS 异步刷盘;读取热数据直接命中页缓存,还避免了 JVM 堆内缓存带来的 GC 压力。这也是为什么 Kafka 机器要留足空闲内存给页缓存,堆不宜过大。
  3. 零拷贝(sendfile):消费者拉取数据时,Broker 用 sendfile 把页缓存里的数据直接送到网卡,省掉“内核态 → 用户态 → 内核态”的两次拷贝与上下文切换。注意 TLS 加密开启后无法使用零拷贝,吞吐会下降。
  4. 批量与压缩:生产者把多条消息攒成 batch(batch.size、linger.ms),整批压缩(compression.type:lz4 / zstd / snappy / gzip),整批写入、整批传输,网络与磁盘利用率大增;Broker 通常不解压,原样存储,消费者解压。
  5. 分区并行:Topic 分成多个分区,分布到不同 Broker,生产与消费都可以并行,吞吐随分区数与机器数线性扩展(在合理范围内)。
  6. 稀疏索引:每个 segment 有 .index(offset 索引)与 .timeindex(时间戳索引),并不是每条消息都建索引,而是隔一定字节建一条(稀疏),查找时用二分找到最近的索引项再顺序扫描,索引小、可整体放进内存。

副本机制:每个分区有一个 leader 和若干 follower,follower 从 leader 拉取数据;ISR(In-Sync Replicas)是与 leader 保持同步的副本集合,落后太多(replica.lag.time.max.ms)会被踢出;leader 挂了从 ISR 中选新 leader。高水位(HW)是 ISR 中所有副本都已复制到的位置,消费者只能读到 HW 之前的消息,保证 leader 切换时已读消息不会“消失”。

消费组与重平衡:同一消费组内,一个分区只被一个消费者消费;成员变化或订阅变化触发重平衡。重平衡期间(传统协议)整个组会暂停消费(“stop the world”)。缓解:使用 Cooperative Sticky 分配策略(增量重平衡)、使用静态成员(group.instance.id)避免重启触发重平衡、合理设置 session.timeout.ms 与 max.poll.interval.ms,并减小单次 max.poll.records 保证处理不超时。Kafka 4.0 起还引入了新一代消费组协议(KIP-848),把协调逻辑移到 Broker 端以减少全组暂停,具体是否启用请看你使用的版本文档。

版本背景:Kafka 4.0 去掉了对 ZooKeeper 的依赖,元数据完全由 KRaft 管理;旧版本仍需 ZooKeeper 或处于迁移期,选型与升级时要确认。

追问链:“Kafka 为什么不适合大量 Topic?”——每个分区是独立的文件目录与索引,大量分区带来大量文件句柄、随机 IO、控制器与 leader 选举压力;而 RocketMQ 所有 Topic 混写同一个 CommitLog,Topic 多也不会退化成随机写。“Kafka 会丢消息吗?”——参数配错就会(acks=1、脏选举、自动提交位点),配对了并且用多副本,在“不同时坏掉多数副本”的前提下不丢。

🌳 高级:Kafka 生产参数与容量

  • 分区数怎么定:按目标吞吐 ÷ 单分区吞吐估算,同时不小于期望的最大消费者并发数,并留 1.5–2 倍余量;分区不是越多越好(文件句柄、选举时间、内存、端到端延迟都会变差),单 Broker 分区数要有上限治理。
  • 磁盘与保留:按“日均写入量 × 保留天数 × 副本数 × 1.2”估算磁盘;用 retention.ms / retention.bytes 控制;关键 Topic 保留时间要覆盖“最长故障恢复 + 消费追平”的时间。
  • 大消息:默认 message.max.bytes 约 1MB,随意调大会拖垮吞吐;大文件应放对象存储,消息里只传引用。
  • Exactly Once 的现实:流处理 Kafka → Kafka 用事务;Kafka → 数据库用“幂等写 / 事务内同时保存位点”实现;不要承诺端到端恰好一次,而是承诺“至少一次 + 效果上的恰好一次(幂等)”。

九、RocketMQ 存储模型

🌿 中级:CommitLog + ConsumeQueue + IndexFile

Producer 发送 Topic A / B / C CommitLog:所有 Topic 的消息混合顺序追加(物理上单一写入流) A-msg1 B-msg1 A-msg2 C-msg1 A-msg3 ... 单个文件默认 1GB,按物理偏移量命名;顺序写 + 页缓存 + 可选同步/异步刷盘 异步构建(ReputMessageService 分发) ConsumeQueue:按 Topic + QueueId 建立的“消息位置索引”,每条固定长度(约 20 字节) 内容:CommitLog 物理偏移量 + 消息大小 + Tag 哈希值;消费者按队列位点顺序读它,再回 CommitLog 取消息体 Tag 哈希用于 Broker 端快速过滤,消息体读取是随机读但多命中页缓存 IndexFile:按消息 Key / 时间范围的哈希索引,用于“按业务 key 查消息”的运维查询 写入靠 CommitLog 的顺序性获得高吞吐,读取靠 ConsumeQueue 获得按队列消费的灵活性

标准回答要点:

  • 写路径:所有 Topic 的消息都顺序追加到同一个 CommitLog(只有一个写入流,写入永远是顺序的,Topic 再多也不会退化);随后后台线程异步把消息的位置信息分发到对应 Topic 与 QueueId 的 ConsumeQueue,以及 IndexFile。
  • 读路径:消费者按队列位点读 ConsumeQueue 里的定长条目,拿到 CommitLog 偏移量后去读消息体。读消息体是随机读,但热点数据通常在页缓存里。
  • 与 Kafka 对比:Kafka 每个分区一个独立日志,写入是“多路顺序写”,分区多了趋近随机写;RocketMQ 是“单路顺序写 + 索引分离”,Topic 数量伸缩性更好,代价是读取多一次间接寻址,且 CommitLog 混合存储让按 Topic 清理与隔离更复杂。
  • “推”还是“拉”:RocketMQ 的 Push 消费本质是长轮询的 Pull:消费者向 Broker 发拉取请求,没有消息则 Broker 挂起请求一段时间,有新消息立刻返回,兼顾实时性与消费者的流控。
  • 消息类型:官方概念文档列出四类——普通(NORMAL)、顺序(FIFO)、延迟(DELAY)、事务(TRANSACTION),5.0 起每个 Topic 只允许一种消息类型。
  • 高可用:主从架构中 Slave 复制 Master 的 CommitLog;传统主从不能自动切换,5.x 提供基于 Raft 的 DLedger / Controller 方案自动选主。

追问链:“RocketMQ 怎么做到消息过滤?”——Tag 过滤:Broker 用 ConsumeQueue 里的 Tag 哈希粗筛,消费者端再比较 Tag 字符串精筛;SQL92 属性过滤在 Broker 端按消息属性计算。“消费位点存在哪?”——集群模式下存在 Broker 端(consumerOffset.json),广播模式存在消费者本地。“为什么会有‘消息重复消费’在 RocketMQ 里更常见?”——消费者扩缩容、重平衡、位点提交是定时异步的(默认约每 5 秒),宕机时最后一段位点没持久化就会重复。

🌳 高级:RocketMQ 落地的几个要点

  • 刷盘与复制的组合:核心交易集群常用“异步刷盘 + 同步双写”或直接用 DLedger 多数派复制,既有较好吞吐,又不因单机掉电丢已确认消息;金融级再叠加同步刷盘。要用压测量化:同一硬件上对比三种组合的 TPS 与 P99,把结果写进选型文档。
  • 消费者线程模型:并发消费的线程池大小(consumeThreadMin/Max)、单次批量(consumeMessageBatchMaxSize)、拉取批量(pullBatchSize)是积压治理的第一批旋钮;调之前先看瓶颈在消费逻辑还是拉取。
  • 消息治理:Topic 与消费组的命名与申请规范;消息大小限制(避免大消息);Key 必填(便于查询与排障);灰度发布时用 Tag 或独立消费组区分。
  • 版本升级:4.x → 5.x 的客户端与 Topic 消息类型校验会变,升级前要评估兼容性并做灰度。

十、RabbitMQ 补充:路由与队列类型

🌱 初级 + 🌿 中级

  • 路由模型:生产者把消息发给 Exchange,Exchange 按类型与 Binding 规则路由到 Queue。direct(routing key 精确匹配)、fanout(广播)、topic(通配符 * 一个词、# 零或多个词)、headers(按头属性)。
  • 队列类型:经典队列(Classic)、仲裁队列(Quorum)、Streams。官方文档建议:需要高可用和不丢消息的长期队列用 Quorum;它不适合临时队列、极低延迟要求、非关键数据、超长积压(官方提到数百万级以上应考虑 Streams)以及大规模扇出。旧的“镜像队列”在新版本中已被仲裁队列取代。
  • Prefetch:消费者未确认消息的上限,太大导致一个慢消费者囤积消息、其他消费者空闲;太小则吞吐受网络往返限制。
  • 流控与内存水位:Broker 内存或磁盘达到阈值会阻塞生产者连接(connection.blocked),生产者要处理这种通知。
  • 与 Kafka 的核心区别:RabbitMQ 是“智能 Broker、简单消费者”(Broker 负责路由与投递状态),消息被确认后删除;Kafka 是“简单 Broker、智能消费者”(消费者自己管位点),消息按保留策略保存,可重复消费。

十一、高频追问速查表

问题一句话答案出处 / 延伸
MQ 有什么用,有什么代价异步、解耦、削峰;代价是复杂度、最终一致、多一个高可用组件先谈场景再谈产品
三款 MQ 怎么选Kafka 大吞吐事件流;RocketMQ 业务消息与事务/延迟;RabbitMQ 灵活路由小规模还要看团队运维能力
消息会丢在哪生产者发送、Broker 存储、消费者确认三段,缺一不可acks=all + min.insync.replicas、手动 ack
acks=all 就一定不丢吗不一定,它等 ISR 而非全部副本,要配 min.insync.replicas 并禁用脏选举JavaGuide 总结的关键点
为什么会重复消费生产者重试、确认丢失、重平衡、位点回退;至少一次必然重复消费端必须幂等
幂等怎么做业务唯一键 + 唯一索引/去重表与业务同事务,或状态机条件更新不要只靠 messageId
怎么保证顺序同业务 key 到同一分区/队列,消费端同分区串行重试、扩分区、多线程会破坏
Kafka 消息乱序原因重试导致乱序,开幂等生产者可在 in-flight ≤ 5 时保持分区有序否则 in-flight 设 1
RocketMQ 事务消息流程半消息 → 本地事务 → Commit/Rollback → 缺失二次确认则回查只保证最终一致,消费端仍需幂等
Kafka 事务与 RocketMQ 事务区别Kafka 保 Kafka 内读-处理-写原子;RocketMQ 保本地事务与发消息原子Kafka EOS 不等于端到端
消息积压怎么办先定位原因;提速(批量/并行)、扩容(消费者≤分区)、搬运到多分区新 Topic、必要时丢弃低价值消息监控 lag 趋势而非绝对值
延迟消息怎么做RocketMQ 原生;RabbitMQ 用 TTL+死信或插件;Kafka 需自建TTL 方案有队头阻塞;消费时核对业务状态
Kafka 为什么快顺序写、页缓存、零拷贝、批量压缩、分区并行、稀疏索引开 TLS 无法零拷贝
RocketMQ 存储模型所有 Topic 混写 CommitLog,ConsumeQueue 做定长位置索引,IndexFile 按 key 查询Topic 多不退化
Rebalance 有什么问题重平衡期间暂停消费并可能重复,用协作式分配与静态成员缓解控制 max.poll.interval.ms
死信队列干什么承接反复失败的消息,避免阻塞;配告警与重放工具Kafka 需自建,RocketMQ 自带 %DLQ%

一句话备忘:不丢靠“发送确认 + 多副本 + 处理后再 ack”,不重靠“消费端幂等(业务唯一键 + 同事务)”,有序靠“同 key 同分区 + 分区内串行”,事务靠“半消息回查或本地消息表”,积压靠“先定位再提速扩容”;没有银弹,只有“至少一次 + 幂等 + 对账”的组合。

十二、收尾

整理完这一章,我觉得 MQ 面试有个隐藏评分标准:你是不是“悲观主义者”。好的回答者默认网络会断、机器会挂、消息会重、消费者会慢,然后一步步说“所以我会怎么设计”。而普通的回答者,默认一切顺利,然后在追问中被一个个“万一”击穿。所以建议你每答一个机制,都补一句“如果它失败了,兜底是什么”——发送失败有本地消息表,消息重复有幂等,事务不确定有回查,一切都有问题有对账。

最后再啰嗦一句版本的事:Kafka 的 ZooKeeper → KRaft、RocketMQ 4.x → 5.x、RabbitMQ 的镜像队列 → 仲裁队列,都是近几年发生的大变化。面试里提到版本差异,是“真的用过、真的关心”的最好证据,但请务必以你实际使用版本的官方文档为准,本文标注“以版本文档为准”的地方尤其如此。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人