
RocketMQ 面试冲刺高频考点一口气背完作者鱼宵 实战驱动系列 · 第 10 篇完整课程与可运行源码已开源在 Giteehttps://gitee.com/j67mk2/rocketmq-journey 本文对应 lesson-10/面试最后一轮面试官放下简历慢悠悠来一句“你们项目里用了 RocketMQ 吧别讲概念——消息发出去万一丢了怎么办”你嘴一张加个重试……然后就卡住了。这不是你没学过是学过和说得出来中间差一张能照着背的地图。这篇是整个系列的收官课不教新 API就干两件事把前 9 课压成一张知识地图 8 条速记面试前过一遍给你一个 5 分钟就能跑起来的回顾 Demo——一个类把同步、异步、延迟三种发送姿势加 Tag 过滤全跑一遍面试前手不生。Demo 源码就在 lesson-10/ 目录下clone 下来自己改参数试。跑出来的现象和我下面贴的实测输出对不上的地方就是你真正没吃透的地方。一、先背这张表九大主题一句话讲完面试官抛出任何一个 RocketMQ 问题你先把它归到下面九个主题里再甩一句结论。这张表背熟等于把前 9 课全装进了脑子#主题一句话结论解决什么问题生活类比对应课1架构组件Producer 发、Broker 存、Consumer 收、NameServer 指路快递体系寄件人 / 仓库 / 收件人 / 电话簿第 1 课2发送方式同步最可靠、异步吞吐高、单向不关心结果延迟/事务是特殊消息当面签收同步vs 放驿站等短信异步第 2 课3消费模式Push 本质是长轮询拉集群组内只消费一次广播每条都收一个包裹只派一人集群vs 人手一份广播第 3 课4可靠性不丢三道保险发送确认 Broker 刷盘/主从同步 消费成功才提交 offset寄件保价、仓库防火、收件人签收第 4 课5顺序消息同一 Queue 内按序消费想全局有序就只用 1 个队列牺牲并行度同一窗口排队先来后到多窗口没法保证全局先后第 5 课6事务消息半消息两阶段先存不投递本地事务成功再提交失败回滚长时间未定则 Broker 回查先交定金放中介那你确认成交了中介才发货第 6 课7延迟消息4.x 固定 18 个级别1s/5s/10s/30s/1m…2h不是任意时间定时炸弹设好级别到点才爆炸投递第 7 课8积压处理先看消费组进度扩容消费者/提高并发实在不行临时加队列分流快递爆仓先加快递员再分新仓库第 8 课9高可用NameServer 多台无状态随便挂Broker 主从复制4.5 可用 Dledger 自动选主电话簿印好几份仓库有正库备份库正库塌了备份顶上第 9 课这张表怎么用面试官问你们怎么保证订单顺序的你先归位到第 5 行——顺序消息——然后把那句结论甩出去同一 Queue 内有序全局有序就得牺牲并行度换。一句话先给答案面试官就知道你是真搭过再让他慢慢追问细节。二、面试官最爱追问的三个为什么这三个问题表面考细节实际考你有没有真的搭过环境、跑过代码。光背概念一追问就露馅。1. 为什么 NameServer 可以无状态、Broker 不行NameServer 只记地址簿地址丢了客户端会重新拉挂一台客户端自动切到另一台Broker 存的是真金白银的消息挂了消息就危险了——所以 Broker 要主从复制 刷盘NameServer 不用。2. 为什么说重复消费是必然的幂等是必须的消费成功后、提交 offset 之前如果消费者宕机重启后会从上一个 offset 再拉一遍——消息就重复了。MQ 只能保证至少投递一次只消费一次得靠业务自己幂等用 msgId 或业务唯一键比如订单号去重。3. 为什么延迟消息只有 18 个级别不能随便设 3 分 20 秒RocketMQ 4.x 的延迟是靠一个固定的延迟级别队列实现的消息先扔进对应级别的队列定时任务到点再搬运回真实 Queue。级别是写死的 18 个想任意时间得用 5.x 的定时消息或者自己用 Redis 过期事件兜底。三、动手回顾三种发送姿势一个类全串了面试前最怕手生。这个回顾 Demo 就一个目的5 分钟把最常考的三种发送姿势再跑一遍。先跑生产者它一共发 6 条消息2 条同步TagA、2 条异步TagB、2 条延迟TagC延迟级别 3 10 秒。packagecom.example.lesson10;importorg.apache.rocketmq.client.producer.DefaultMQProducer;importorg.apache.rocketmq.client.producer.SendCallback;importorg.apache.rocketmq.client.producer.SendResult;importorg.apache.rocketmq.common.message.Message;importjava.nio.charset.StandardCharsets;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.TimeUnit;/** * 第 10 课面试冲刺Demo全课回顾生产者 * 干的事一个类里把前 9 课最常考的三种发送姿势全部演练一遍—— * 1) 同步普通消息回顾第 1、2 课发一条等 Broker 确认一条最可靠 * 2) 异步消息回顾第 2 课发出去不等结果Broker 处理完后回调通知吞吐高 * 3) 延迟消息回顾第 7 课发出去 10 秒后才会被消费者看到常用于超时未支付自动关单。 * 所有消息都发到 TopicLesson10用 TagA / TagB / TagC 三种标签区分回顾第 9 课 Tag 过滤。 */publicclassRecapProducer{publicstaticvoidmain(String[]args)throwsException{// 1. 创建生产者组名 lesson10_producer_group每课独立命名互不干扰DefaultMQProducerproducernewDefaultMQProducer(lesson10_producer_group);// 2. 告诉生产者 NameServer 在哪电话簿地址客户端靠它找到 Brokerproducer.setNamesrvAddr(127.0.0.1:9876);// 3. 启动生产者内部去 NameServer 拉 Broker 地址、建好连接producer.start();System.out.println( 回顾 Demo 生产者已启动开始发送三类消息 );// 第一类同步普通消息TagA回顾第 1、2 课 for(inti0;i2;i){Stringbody【同步消息】全课回顾 第(i1)条;MessagemsgnewMessage(TopicLesson10,TagA,body.getBytes(StandardCharsets.UTF_8));// send() 同步阻塞直到 Broker 落盘并返回 SendResultSEND_OK 才算成功SendResultresultproducer.send(msg);System.out.println([同步] 发送成功body result.getSendStatus()落在队列 queueIdresult.getMessageQueue().getQueueId());}// 第二类异步消息TagB回顾第 2 课 // 用 CountDownLatch 等两个异步回调都完成再关生产者避免程序提前退出导致回调丢失CountDownLatchasyncLatchnewCountDownLatch(2);for(inti0;i2;i){Stringbody【异步消息】全课回顾 第(i1)条;MessagemsgnewMessage(TopicLesson10,TagB,body.getBytes(StandardCharsets.UTF_8));// send(msg, callback)发完立即返回不等 BrokerBroker 处理完后自动回调 callbackproducer.send(msg,newSendCallback(){OverridepublicvoidonSuccess(SendResultresult){System.out.println([异步] 回调成功body result.getSendStatus());asyncLatch.countDown();}OverridepublicvoidonException(Throwablee){System.err.println([异步] 回调失败body e.getMessage());asyncLatch.countDown();}});System.out.println([异步] 已投递发送请求不等结果body);}// 最多等 10 秒让两个异步回调跑完asyncLatch.await(10,TimeUnit.SECONDS);// 第三类延迟消息TagC回顾第 7 课 // delayLevel3 表示延迟 10 秒RocketMQ 4.x 固定 18 个级别11s 25s 310s ...// 这 2 条消息 Broker 会先存着10 秒后才允许消费者拉到——正好用来观察延迟效果for(inti0;i2;i){Stringbody【延迟消息】全课回顾 第(i1)条延迟10秒;MessagemsgnewMessage(TopicLesson10,TagC,body.getBytes(StandardCharsets.UTF_8));msg.setDelayTimeLevel(3);SendResultresultproducer.send(msg);System.out.println([延迟] 已发送10秒后才可见body result.getSendStatus());}// 4. 关闭生产者释放连接producer.shutdown();System.out.println( 全部发送完毕生产者已关闭共 6 条2同步 2异步 2延迟 );}}这三块就是面试发送方式怎么选的标准答案同步producer.send(msg)阻塞等回执SEND_OK才算成功。适合订单、扣款这种错一条都不行的场景。异步producer.send(msg, callback)发完立刻返回Broker 处理完后回调onSuccess/onException。适合接口响应时间敏感、又不想丢消息的场景比如注册后发优惠券。注意回调在别的线程里跑不能直接操作主线程变量所以这里用了CountDownLatch等回调完成再关生产者。延迟msg.setDelayTimeLevel(3)数字 3 延迟 10 秒。4.x 只有 18 个固定级别背一下前几个11s、25s、310s、430s、51min、162h。Tag第二个参数 “TagA”同一个 Topic 里给消息分类消费者订阅时按 Tag 过滤不用为每种小业务都建一个 Topic。四、消费者一行 subscribe三种 Tag 全收消费者订阅 TopicLesson10第二个参数传*表示 Tag 全收。它从最早消息开始消费CONSUME_FROM_FIRST_OFFSET这样先发消息再启动消费者也能把历史消息捞回来收满 6 条自动退出最多等 35 秒——因为有 2 条延迟消息要 10 秒后才可见。packagecom.example.lesson10;importorg.apache.rocketmq.client.consumer.DefaultMQPushConsumer;importorg.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;importorg.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;importorg.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;importorg.apache.rocketmq.common.consumer.ConsumeFromWhere;importorg.apache.rocketmq.common.message.MessageExt;importjava.nio.charset.StandardCharsets;importjava.util.List;importjava.util.concurrent.atomic.AtomicInteger;/** * 第 10 课面试冲刺Demo全课回顾消费者 * 干的事订阅 TopicLesson10 的全部 Tag*把生产者发的 6 条消息2同步2异步2延迟 * 全部收下来并打印顺带打印每条消息落到的队列 queueId方便回顾消息分散在多个队列。 * 设计要点 * - 从最早消息开始消费CONSUME_FROM_FIRST_OFFSET这样先发后启动消费者也能捞回历史消息 * - 因为有 2 条延迟消息要等 10 秒才可见退出窗口留足 35 秒收满 6 条也会提前退出。 * - 平时生产环境消费者是常驻进程不会自己退这个自动退出只是为了教程演示不挂住终端。 */publicclassRecapConsumer{publicstaticvoidmain(String[]args)throwsException{// 1. 创建 Push 消费者组名 lesson10_consumer_group同一组内一条消息只被组里一个实例消费DefaultMQPushConsumerconsumernewDefaultMQPushConsumer(lesson10_consumer_group);// 2. 告诉消费者 NameServer 在哪consumer.setNamesrvAddr(127.0.0.1:9876);// 3. 从最早的消息开始消费保证能收到先发后启动的历史消息consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);// 4. 订阅 TopicLesson10第二个参数 * 表示不过滤 TagTagA/TagB/TagC 全都收// 第 9 课讲过这里填 TagA || TagC 就只收指定标签SQL92 过滤更灵活consumer.subscribe(TopicLesson10,*);// 5. 注册监听器Broker 推消息过来就回调这个方法打印内容和 TagAtomicIntegerreceivedCountnewAtomicInteger(0);consumer.registerMessageListener(newMessageListenerConcurrently(){OverridepublicConsumeConcurrentlyStatusconsumeMessage(ListMessageExtmsgs,ConsumeConcurrentlyContextcontext){for(MessageExtmsg:msgs){StringbodyTextnewString(msg.getBody(),StandardCharsets.UTF_8);// queueId这条消息落在哪个队列bornTimestamp发送时间可观察延迟消息晚到System.out.println(收到消息: bodyTexttagmsg.getTags()queueIdmsg.getQueueId());receivedCount.incrementAndGet();}// 返回 CONSUME_SUCCESS 处理成功可以确认进度offset了returnConsumeConcurrentlyStatus.CONSUME_SUCCESS;}});// 6. 启动消费者consumer.start();System.out.println(消费者已启动正在等待消息……收满 6 条自动退出最多等 35 秒延迟消息约 10 秒后才到);// 7. 演示用退出逻辑收满 6 条或 35 秒超时就关闭消费者longdeadlineSystem.currentTimeMillis()35_000;while(receivedCount.get()6System.currentTimeMillis()deadline){Thread.sleep(500);}consumer.shutdown();System.out.println(观察结束共收到 receivedCount.get() 条消息消费者已关闭。);}}跑之前先把三件套起好NameServer、Broker、Dashboardcd rocketmq-journey docker compose up-d$env:JAVA_HOMEC:\Program Files\Java\jdk-17cd lesson-10 mvn clean install-DskipTests[Console]::OutputEncoding[System.Text.Encoding]::UTF8$env:JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8mvn exec:java-Dexec.mainClasscom.example.lesson10.RecapProducer# 生产者跑完再开一个窗口跑消费者mvn exec:java-Dexec.mainClasscom.example.lesson10.RecapConsumersubscribe第二个参数就是 Tag 过滤表达式*全收TagA || TagC只收这两个标签还支持 SQL92 按消息属性过滤——这是第 9 课 Tag vs SQL92 的考点。返回CONSUME_SUCCESS等于告诉 Broker我处理完了把 offset 往前推如果返回RECONSUME_LATER这条消息进重试队列重试 N 次后进死信队列DLQ那是第 4 课的考点。五、实测输出这堆日志里藏着两个面试点以下是本机Windows 11 Docker 29 JDK 17真实运行输出你 clone 下来跑应该和这个对得上。生产者输出 回顾 Demo 生产者已启动开始发送三类消息 [同步] 发送成功【同步消息】全课回顾 第1条 SEND_OK落在队列 queueId1 [同步] 发送成功【同步消息】全课回顾 第2条 SEND_OK落在队列 queueId2 [异步] 已投递发送请求不等结果【异步消息】全课回顾 第1条 [异步] 已投递发送请求不等结果【异步消息】全课回顾 第2条 [异步] 回调成功【异步消息】全课回顾 第1条 SEND_OK [异步] 回调成功【异步消息】全课回顾 第2条 SEND_OK [延迟] 已发送10秒后才可见【延迟消息】全课回顾 第1条延迟10秒 SEND_OK [延迟] 已发送10秒后才可见【延迟消息】全课回顾 第2条延迟10秒 SEND_OK 全部发送完毕生产者已关闭共 6 条2同步 2异步 2延迟 消费者输出消费者已启动正在等待消息……收满 6 条自动退出最多等 35 秒延迟消息约 10 秒后才到 收到消息: 【延迟消息】全课回顾 第2条延迟10秒tagTagCqueueId0 收到消息: 【异步消息】全课回顾 第2条tagTagBqueueId0 收到消息: 【同步消息】全课回顾 第2条tagTagAqueueId2 收到消息: 【同步消息】全课回顾 第1条tagTagAqueueId1 收到消息: 【延迟消息】全课回顾 第1条延迟10秒tagTagCqueueId3 收到消息: 【异步消息】全课回顾 第1条tagTagBqueueId0 观察结束共收到 6 条消息消费者已关闭。看懂这份输出现象说明6 条全部 SEND_OK三种发送姿势全部发送成功SEND_OK 是最可靠的发送状态同步两条落在 queueId1、2消息被轮询散到不同队列这是默认队列选择算法异步已投递先打印、回调成功后打印证明异步不等 Broker主线程立即继续跑回执晚到一会儿才回调延迟消息也立刻 SEND_OKSEND_OK 只代表Broker 收下了不代表消费者马上能看到——延迟消息要 10 秒后才可见输出里还藏着两个面试点Tag 全收了订阅传*TagA/TagB/TagC 一个不落改成TagA就只收 2 条同步消息——下面挑战题第 1 题让你亲手验证。消费顺序 ≠ 发送顺序发送顺序是同步1→同步2→异步1→异步2→延迟1→延迟2收下来却是乱的——6 条消息散在 queueId 0/1/2/3 四个队列每个队列内部有序队列之间并行消费就乱了。这就是第 5 课顺序消息的本质顺序性只在单个 Queue 内成立。六、线上踩坑清单每一条都是面试题面试官最爱问你们线上 MQ 出过什么问题怎么排查的——你亲手踩过的这些坑就是最好的 STAR 素材。这张表把前 9 课的坑汇总了一遍#坑现象原因一句话解决办法1brokerIP1 配错Broker 注册了客户端却 connect failedWindowsWSL2 下 Docker 端口只绑在回环和 WSL IPbroker.conf 里 brokerIP1 填 WSL 虚拟机 IP2Broker JDK cgroup 崩溃最隐蔽生产者 SEND_OK、控制台正常消费者永远收不到JDK8 在 cgroup v2 下读容器内存抛 NPE类初始化失败JVM 加 -XX:-UseContainerSupport3Maven 跑在 JDK 8 上invalid target release: 17Maven 本体默认用 JDK8 编译器每条 mvn 命令前设 JAVA_HOME 指向 174PowerShell 拆参数mvn exec:java 报 Unknown lifecycle phasePowerShell 把带点的 -D… 拆开了整个参数加引号5中文乱码控制台打印中文变 ???PowerShell 5.1 默认 GBKJVM 默认 UTF-8运行前设 OutputEncoding 和 file.encodingUTF-86先发后启动消费者收不到消息消息发出去了消费者一条没有默认从最新开始消费历史消息被跳过教程用 CONSUME_FROM_FIRST_OFFSET生产靠 offset 续传7异步回调拿不到结果异步发送后程序立刻退出回调没执行主线程不等回调就 shutdown 了用 CountDownLatch 等回调8延迟消息没生效发了延迟消息消费者立刻就收到启动时距发送已超过延迟时长或延迟级别填了 0延迟级别 1~18310s接收窗口留足延迟时长9消费顺序是乱的消息按队列并行消费全局顺序乱多队列并行 牺牲全局顺序换吞吐要全局有序单队列 MessageListenerOrderly10端口冲突控制台/Broker 起不来8080、9876 等被别的程序占用改 compose 端口映射七、挑战题答案在仓库源码里跑起来才知道⭐ 把 RecapConsumer 的订阅从*改成TagA重新运行不用重发消息CONSUME_FROM_FIRST_OFFSET会把历史消息再捞一遍——观察是不是只收到 2 条同步消息。亲手验证 Tag 过滤。⭐⭐ 把 RecapProducer 里延迟消息的setDelayTimeLevel(3)改成setDelayTimeLevel(1)1 秒再运行消费者对比延迟消息什么时候才出现然后试试传0猜猜会发生什么⭐⭐⭐ 现在 6 条消息散在 4 个队列里收下来顺序是乱的。面试官追问“那要做到全局严格有序代码得改哪两处”——提示队列数量 监听器换成顺序消费模式MessageListenerOrderly第 5 课有完整代码。八、面试回答模板结论先行再展开完整 30 道题在仓库docs/RocketMQ高频面试30问.md每题都有结论一句话 展开 面试提示。这里先把最核心的 8 条刻进脑子面试题一句话答案为什么用 MQ解耦、异步、削峰代价是延迟、复杂度、可能重复消费Kafka / RabbitMQ / RocketMQ 怎么选日志流找 Kafka吞吐王轻量路由找 RabbitMQ业务消息事务可靠性优先 RocketMQNameServer 为什么无状态也能高可用它只存地址簿客户端会重新拉挂一台客户端自动切别的消息不丢靠哪三环节生产者确认发送 Broker 同步刷盘/主从同步 消费者处理成功才提交 offset重复消费怎么办MQ 只保证至少一次业务必须幂等msgId/业务唯一键去重顺序消息怎么保证同一 Queue 内有序全局有序 单队列 顺序消费牺牲并行度事务消息原理半消息两阶段先存不投递本地事务成功再提交失败回滚长时间未定则 Broker 回查积压了怎么处理先定位是生产快还是消费慢扩容消费者实例不超过队列数 提高消费并发临时加队列分流再给你三段高频追问的标准答法注意每段都是先甩结论面试官你们线上消息积压了怎么处理结论先定位是生产太快还是消费太慢再对症下药。展开先去 Dashboard 看消费组的 offset 追没追上如果是消费慢优先扩容消费者实例注意实例数别超过队列数多出来的实例闲着、提高消费线程并发实在顶不住就临时建更多队列分流。这次回顾 Demo 的输出里能看到消息散在 4 个队列——队列数决定了集群消费最多能并行到几个实例。面试官事务消息是怎么保证订单和积分一致的结论两阶段提交 本地事务执行 Broker 定时回查。展开先发半消息到 Broker存起来但不投递Broker 确认后回调执行本地事务写订单库成功就提交、失败就回滚万一本地事务状态一直没上报Broker 会定期回查你那个事务到底成没成最终一致。对应第 6 课。面试官延迟消息能设 3 分 20 秒吗结论4.x 不行只有 18 个固定级别。展开它是靠固定的延迟级别队列实现的消息先扔进对应级别队列定时任务到点再搬回真实 Queue。要任意时间得用 5.x 的定时消息或者自己用 Redis 过期事件兜底——第 7 课就是用它做30 分钟未支付自动关单。关于这个系列本文是「Java 后端实战精通营」系列第 10 篇原则实战驱动、由浅到深、面试向每篇文章的结论都可以亲手验证。RocketMQ 实战精通营10 课https://gitee.com/j67mk2/rocketmq-journey本文对应源码位置lesson-10/一个回顾工程RecapProducer 发 6 条消息 RecapConsumer 全 Tag 接收5 分钟跑完全课核心docs/目录还有完整的高频 30 问和简历话术系列文章一览按发布顺序篇主题1RocketMQ 入门Docker 一行起三件套跑通你的第一条消息2RocketMQ 发送方式同步异步批量单向消息都怎么发出去3RocketMQ 消费模式集群、广播与重试消息怎么被吃掉4RocketMQ 可靠性发送重试加幂等消息一条都不丢5RocketMQ 顺序消息订单流程不乱套的秘密6RocketMQ 事务消息订单与积分的最终一致7RocketMQ 延迟消息30 分钟未支付自动关单怎么做8RocketMQ 积压治理百万消息堵在队列怎么办9RocketMQ 集群高可用与过滤主从架构 Tag 精准投递10RocketMQ 面试冲刺高频考点一口气背完系列完结十篇走完你已经能从 Docker 起一套 RocketMQ、写得出同步/异步/延迟/事务消息、讲得清不丢消息和顺序消费的原理、遇到积压和乱码知道去哪查。后续想再往上走读一遍DefaultMQProducer.send()主流程源码、了解 5.x 的 Proxy 存算分离、在两台机器上搭一主一丛亲手 kill 掉主节点观察切换。最后一句——MQ 的本质不是某个中间件而是用排队换稳定、用异步换解耦这套思维方式。把它带走以后换 Kafka、换 RabbitMQ你都是最快上手的那个人。跑完有任何报错把终端输出发评论区一起排查。