MQ 面试防 “翻车” 指南:深挖 RocketMQ,把面试官问的都怼回去
面向 Java 后端开发,涵盖选型、可靠性、顺序性、幂等、积压、高可用、事务消息等高频面试题。 采用「底层原理 → 源码实现 → 面试标准答案 → 追问 → 业务落地」五段式结构。
目录
1. 什么是消息队列
1.1 底层原理
消息队列(Message Queue)本质上是一个保存消息的 FIFO(先进先出)数据结构,跨进程的通信机制。底层核心逻辑是通过"存储-转发"机制,将消息暂存于 Broker(消息服务器),避免生产者与消费者直接耦合,同时提供消息的持久化、路由、重试等能力,解决分布式系统中的通信问题。
核心模型对比:
| 模型 | 说明 | 典型实现 |
|---|---|---|
| 点对点(P2P) | 一条消息只能被一个消费者消费 | RabbitMQ(Queue模式) |
| 发布/订阅(Pub/Sub) | 一条消息可被多个消费者组消费 | RocketMQ(Topic模式)、Kafka |
1.2 RocketMQ 实现
RocketMQ 的核心架构由 4 部分组成,完美实现消息队列的"存储-转发"逻辑:
-
Producer(生产者):负责发送消息,支持同步、异步、单向发送三种模式;
-
Consumer(消费者):负责接收并处理消息,支持集群消费、广播消费两种模式;
-
Broker(消息服务器):核心组件,负责消息的存储、转发、持久化,基于磁盘文件(CommitLog)存储,兼顾性能和可靠性;
-
NameServer(命名服务器):无状态节点,负责路由管理,类似"路由导航",Broker 启动后向其注册自身信息,Producer/Consumer 从中获取路由信息。
架构交互流程:
Producer ──注册──→ NameServer ←──注册── Broker (Master/Slave)
│ ↑ ↑
│──发送消息──→ Broker ──主从同步──→ Slave
│ │
Consumer ←──拉取消息──┘
1.3 面试标准答案
消息队列是一种基于发布/订阅模式的异步通信中间件,核心作用是将同步的系统调用转化为异步的消息传递,从而打破系统之间的强耦合关系。RocketMQ 通过 Producer、Consumer、Broker、NameServer 四大组件协作,实现了高性能的解耦、异步和削峰,且自身不依赖 ZooKeeper,部署轻量简便。
1.4 追问
Q:RocketMQ 的 Producer 和 Consumer 分别与哪些组件交互?
A:Producer 启动时从 NameServer 拉取 Topic 路由信息(包含 Broker 地址和 Queue 列表),然后直接与 Broker Master 建立 TCP 长连接发送消息。Consumer 同样从 NameServer 拉取路由,但可以连接 Master 和 Slave 拉取消息(当
slaveReadEnable=true时,Slave 可分担读压力)。
Q:NameServer 是无状态的,那路由信息存在哪?
A:路由信息只存在于 NameServer 的内存中(
RouteInfoManager),不持久化。Broker 每 30 秒向所有 NameServer 发送心跳,NameServer 通过定时任务每 10 秒检测 Broker 存活,超过 120 秒无心跳则剔除。Producer/Consumer 每 30 秒从 NameServer 更新一次本地路由表。
1.5 业务落地
电商下单全链路。用户下单后,如果同步执行"扣减库存、生成订单、发短信、加积分",不仅耗时长,而且任何一环报错都会导致下单失败。引入 RocketMQ 后,订单服务仅需"生成订单"并将后续任务作为消息发给 MQ,立刻返回成功给用户。下游库存、短信、积分服务异步拉取消费,接口响应时间从几百毫秒骤降至几十毫秒,各服务故障互不影响。
2. 消息队列有哪些使用场景
2.1 底层原理
MQ 的核心使用场景可以用三个词概括:解耦、异步、削峰填谷,对应分布式系统中耦合度高、响应慢、流量突发三大痛点。
-
解耦:系统间不再相互调用 API,而是通过 MQ 传递数据,新增下游业务无需改动上游代码,降低系统耦合度;
-
异步:主流程以外的非核心链路,扔给 MQ 后台异步处理,无需同步等待,大幅提升主链路响应速度;
-
削峰填谷:面对瞬时超大流量,利用 MQ 的暂存能力,将流量拦截在数据库之前,后台按数据库能承受的速率平滑拉取处理,避免系统被压垮。
2.2 场景详解与 RocketMQ 适配
| 场景 | 问题 | RocketMQ 解决方案 | 典型案例 |
|---|---|---|---|
| 异步通信 | 同步调用耗时长 | 异步发送 + 回调确认 | 用户注册后异步发短信/邮件 |
| 削峰填谷 | 瞬时流量压垮 DB | 高吞吐暂存 + 匀速消费 | 双11 秒杀请求缓冲 |
| 系统解耦 | 上下游强依赖 | Topic 订阅模式 | 订单→物流/积分/发票 |
| 分布式事务 | 跨服务数据不一致 | 事务消息(半消息+回查) | 转账、支付回调 |
| 日志/数据同步 | 同步传输影响性能 | 批量发送 + 异步传输 | Binlog 同步、埋点上报 |
2.3 追问
Q:解耦和异步有什么区别?
A:解耦强调的是"我不需要知道你是谁"——通过 MQ 的 Topic 订阅,上游只管发消息,下游可以自由增减,互不感知。异步强调的是"我不需要等你做完"——主流程发完消息就返回,耗时操作交给消费者后台处理。两者经常同时出现,但本质不同。
Q:削峰填谷时,如果消息量持续超过消费能力怎么办?
A:这属于"谷填不满"的情况,需要配合限流 + 扩容 + 降级三板斧:生产端限流控制发送速率;消费端扩容消费者实例和队列数;非核心业务降级,只处理核心消息。
2.4 业务落地
电商秒杀系统。用户点击抢购,网关直接将请求消息发送到 RocketMQ,并立即向用户返回"排队中",无需等待后续业务处理。后台订单服务按照数据库承载力(如每秒落库 2000 单),从 RocketMQ 平滑拉取消息,执行扣减库存、创建订单的核心逻辑,完美保护底层数据库和服务不被瞬时流量压垮。
3. 消息队列如何解决消息丢失问题
3.1 底层原理
一条消息的流转经历三个核心阶段:生产者 → Broker(MQ 服务器) → 消费者。要保证消息不丢失,必须在这三个环节都做到"确认机制 + 持久化 + 重试兜底",覆盖网络抖动、服务宕机等异常场景。
消息丢失的三种情况:
| 环节 | 丢失原因 | 后果 |
|---|---|---|
| 生产者→Broker | 网络抖动、Broker 宕机 | 消息根本没到达 MQ |
| Broker 存储 | 内存未刷盘、单机宕机 | 消息到了但没存住 |
| Broker→消费者 | 消费者处理完但 ACK 前宕机 | 消息被重复投递或丢失 |
3.2 RocketMQ 实现(重点)
RocketMQ 可靠性极高,针对三个环节分别设计保障机制:
环节一:生产者(避免发送丢失)
// ✅ 同步发送(默认,推荐)—— 等待 Broker 返回 SEND_OK
SendResult result = producer.send(msg);
// ❌ 单向发送(不推荐)—— 无返回确认,消息可能丢失
producer.sendOneway(msg);
// ✅ 异步发送 + 回调(高吞吐推荐)
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult result) { /* 发送成功 */ }
@Override
public void onException(Throwable e) { /* 发送失败,重试或记录 */ }
});
-
同步发送(Sync)模式(默认),Producer 发送消息后,必须等待 Broker 返回"发送成功"(
SEND_OK)才算成功,失败会自动重试(默认 3 次); -
禁止使用单向发送(无返回确认),避免消息发送失败无法感知。
环节二:Broker(避免存储丢失)
# broker.conf 关键配置
flushDiskType=SYNC_FLUSH # 同步刷盘(默认 ASYNC_FLUSH)
brokerRole=SYNC_MASTER # 同步复制(默认 ASYNC_MASTER)
-
配置同步刷盘(
flushDiskType=SYNC_FLUSH),消息写入 CommitLog 磁盘文件后才返回成功,避免内存中消息因宕机丢失; -
配置主从同步复制(
brokerRole=SYNC_MASTER),Master 收到消息后同步到 Slave,确保主节点宕机后数据不丢失。
环节三:消费者(避免消费丢失)
// ✅ 手动 ACK —— 处理完业务再确认
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
try {
// 执行业务逻辑
processBusiness(msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 确认消费成功
} catch (Exception e) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER; // 消费失败,稍后重试
}
});
-
关闭自动 ACK,使用手动 ACK 确认机制;
-
只有业务逻辑处理完毕,才返回
CONSUME_SUCCESS,否则 Broker 会自动重试,避免未处理完消息就确认导致丢失。
3.3 三环节对比表
| 环节 | 保障机制 | 配置/代码 | 性能影响 |
|---|---|---|---|
| 生产者 | 同步发送 + 重试(默认3次) | producer.send(msg) |
中等(等待响应) |
| Broker 存储 | 同步刷盘 | flushDiskType=SYNC_FLUSH |
高(写磁盘后才返回) |
| Broker 复制 | 同步主从复制 | brokerRole=SYNC_MASTER |
中等(等 Slave 确认) |
| 消费者 | 手动 ACK | RECONSUME_LATER |
低 |
性能与可靠性的权衡:
-
最高可靠:同步发送 + 同步刷盘 + 同步复制 → 金融级,吞吐量最低
-
推荐配置:同步发送 + 异步刷盘 + 同步复制 → 平衡方案
-
最高性能:异步发送 + 异步刷盘 + 异步复制 → 日志级,可能丢少量数据
3.4 追问
Q:同步刷盘和异步刷盘的区别是什么?
A:同步刷盘是消息写入 PageCache 后,立即调用
fsync强制刷到磁盘物理介质,然后才返回成功,可靠性最高但性能差。异步刷盘是消息写入 PageCache 后就返回成功,由后台线程定时(默认 500ms)批量刷盘,性能高但宕机可能丢失最后一批数据。RocketMQ 还支持异步刷盘+主从同步复制的折中方案,即使主节点宕机,Slave 上还有数据。
Q:RocketMQ 的 CommitLog 是怎么存储的?
A:所有 Topic 的消息都顺序写入同一个 CommitLog 文件(单个 1GB),通过内存映射(mmap)实现高效写入。消费时通过 ConsumeQueue(逻辑队列,类似 Kafka 的 Partition)索引到 CommitLog 的物理偏移量,再读取消息体。这种"集中存储 + 索引分离"的设计,写入是纯粹的顺序 I/O,性能极高。
Q:如果 Broker 开了同步刷盘,性能会下降多少?
A:根据阿里官方压测,同步刷盘相比异步刷盘,TPS 大约下降 50%~70%。例如异步刷盘可达 10 万+ TPS,同步刷盘大约 3~5 万 TPS。金融场景通常采用"异步刷盘 + 同步复制"的折中方案,既保证数据不丢(Slave 有副本),又不至于性能太差。
3.5 业务落地
金融支付转账通知场景(核心要求消息零丢失):资金链路容不得半点丢失,需牺牲部分吞吐量,在 Broker 端强制开启同步刷盘和同步主从复制;消费端(对账服务)处理完对账逻辑(比对支付金额、订单信息)后,再发送手动 ACK;若处理异常,返回消费失败,Broker 触发重试,同时将失败消息打入死信队列(DLQ),后续人工干预排查。
4. 消息队列如何保证消息的顺序性
4.1 底层原理
顺序性分为全局顺序和局部(分区)顺序:
| 类型 | 说明 | 实现方式 | 吞吐量 | 适用场景 |
|---|---|---|---|---|
| 全局顺序 | 所有消息严格 FIFO | 单 Queue/Partition | 极低 | 几乎不用 |
| 局部顺序 | 同一业务实体的消息有序 | Hash 路由到同一 Queue | 高 | 主流方案 |
核心解决思路:将需要保证顺序的消息,路由到同一个队列/分区,且保证该队列/分区的消息"串行发送、串行消费",避免并发导致顺序错乱。
4.2 RocketMQ 实现(重点)
生产端保证
// 使用队列选择器,将同一订单的消息路由到同一个 Queue
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long orderId = (Long) arg;
int index = (int) (orderId % mqs.size()); // Hash 取模
return mqs.get(index);
}
}, orderId); // 传入业务唯一标识
-
使用队列选择器(
MessageQueueSelector),对业务唯一标识(如订单 ID)进行 Hash 取模,相同标识的消息会被路由到同一个 Topic 下的同一个 Queue; -
同时采用串行发送,避免同一标识消息并发发送到不同队列。
Broker 端保证
-
同一 MessageQueue 内的消息,严格按照发送顺序写入 CommitLog;
-
转发到 ConsumerQueue 时保持顺序不变;
-
禁止对顺序消息队列进行负载均衡调整,避免顺序错乱。
消费端保证
// 使用顺序消费监听器(而非并发消费监听器)
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 单线程串行消费同一 Queue 的消息
processOrder(msgs);
return ConsumeOrderlyStatus.SUCCESS;
}
});
-
使用
MessageListenerOrderly接口(而非MessageListenerConcurrently); -
Broker 会给对应的 Queue 加分布式锁,保证同一时间只有一个消费者线程拉取和处理该 Queue 的消息,实现单线程串行消费;
-
消费失败时,暂停该队列消费,重试成功后再处理下一条,避免顺序错乱。
4.3 顺序消息完整流程图
Producer Broker Consumer
│ │ │
│──orderId=1001──→ Queue0 ──→ CommitLog ──→ ConsumerQueue0 ──→ 线程A(串行)
│──orderId=1001──→ Queue0 ──→ │ │ │
│──orderId=1002──→ Queue1 ──→ CommitLog ──→ ConsumerQueue1 ──→ 线程B(串行)
│ │ │
│ Hash(orderId) % Queue数 │ 严格写入顺序 │ 分布式锁保证
│ 相同orderId → 同一Queue │ 不乱序 │ 单线程消费
4.4 追问
Q:全局顺序怎么实现?有必要吗?
A:全局顺序只需将 Topic 的 Queue 数设为 1,所有消息都进同一个 Queue,天然全局有序。但这会将吞吐量降到单线程水平,实际业务中极少使用。真需要全局顺序的场景(如 Binlog 同步),通常用局部顺序就够了——按表名+主键 Hash 路由即可。
Q:顺序消费时,某个消息消费失败怎么办?会不会阻塞整个队列?
A:会的。
MessageListenerOrderly模式下,消费失败会暂停当前 Queue 的消费,进入重试。可以通过setMaxReconsumeTimes设置最大重试次数,超过后进入死信队列。这就是顺序消费的代价——一个消息卡住,整个队列都卡住。所以顺序消费的业务逻辑要尽量简单、快速,避免长事务。
Q:RocketMQ 的顺序消息和 Kafka 的顺序消息有什么区别?
A:Kafka 的 Partition 内天然有序(单 Partition 单消费者线程),但没有内置的顺序消费接口,需要业务自己保证单线程消费。RocketMQ 提供了
MessageListenerOrderly接口,内置了分布式锁和单线程消费机制,对开发者更友好。
4.5 业务落地
MySQL Binlog 同步系统(Canal + RocketMQ):一条数据库记录的 Insert、Update、Delete 操作绝对不能乱序,否则同步到下游数据仓库的数据会出错。解决方案:用"表名+主键 ID"进行 Hash 取模,将同一记录的所有操作消息路由到同一个 Queue,严格顺序消费,确保数据同步准确。
5. 消息队列有可能发生重复消费,如何避免,如何做到幂等
5.1 底层原理
由于网络的不可靠性(网络抖动、闪断),主流 MQ(包括 Kafka 和 RocketMQ)默认提供的都是 At Least Once(至少一次) 交付语义。
重复消费的根本原因:
消费者处理完消息 → 准备提交 ACK → 网络闪断 → Broker 未收到 ACK
→ Broker 超时后重新投递 → 消费者再次收到同一条消息 → 重复消费
核心结论:重复消费无法彻底避免,我们要做的不是阻止重复投递,而是保证消费逻辑的幂等性——即多次消费同一消息,业务结果一致,不产生副作用。
三种消息语义对比:
| 语义 | 说明 | 实现难度 | 消息丢失 | 重复消费 |
|---|---|---|---|---|
| At Most Once | 最多一次 | 简单 | 可能丢 | 不重复 |
| At Least Once | 至少一次 | 中等 | 不丢 | 可能重复 |
| Exactly Once | 恰好一次 | 极难 | 不丢 | 不重复 |
RocketMQ 和 Kafka 都是 At Least Once。Kafka 的 "Exactly Once" 仅限于 Kafka 内部(幂等 Producer + 事务),跨系统仍需业务幂等。
5.2 RocketMQ 实现(避免 + 幂等)
避免重复消费(辅助手段)
-
生产者减少重试次数,异步发送时做好回调确认,避免重复触发发送;
-
消费者及时发送 ACK,优化消费逻辑,减少处理耗时,降低宕机概率;
-
Broker 优化投递机制,重试间隔递增,减少短时间内重复投递。
实现幂等(核心方案)
RocketMQ 每条消息有全局唯一的 msgId,生产者也可通过 msg.setKeys() 设置业务唯一标识(如订单 ID),消费者利用该标识实现幂等:
方案 1:唯一标识 + Redis 缓存记录(高并发常用)
String key = "mq:deduplicate:" + msg.getKeys(); // 业务唯一标识
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (Boolean.TRUE.equals(isNew)) {
// 首次消费,执行业务逻辑
processBusiness(msg);
} else {
// 重复消息,直接跳过
log.warn("重复消息: {}", msg.getKeys());
}
方案 2:数据库唯一约束(最可靠)
-- 将业务唯一标识作为唯一索引
CREATE UNIQUE INDEX uk_pay_no ON t_order(pay_no);
-- 重复消费时插入会触发 DuplicateKeyException
-- 捕获异常视为消费成功
方案 3:业务逻辑幂等(最灵活)
-- 更新操作增加条件判断
UPDATE orders SET status = '已支付'
WHERE order_id = 'xxx' AND status = '未支付';
-- 第二次执行时 status 已变,影响行数为 0,自然幂等
5.3 三种方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Redis 去重 | 高并发、允许短暂不一致 | 性能极高、实现简单 | Redis 宕机可能失效 |
| DB 唯一约束 | 金融级、强一致要求 | 绝对可靠 | 性能略低 |
| 业务幂等 | 更新类操作 | 无额外依赖 | 需要业务配合设计 |
5.4 追问
Q:Redis 去重的 Key 过期时间怎么设?
A:过期时间应大于消息的最大重试周期。RocketMQ 默认重试 16 次,最长时间约 4.5 小时,所以 Redis Key 过期时间建议设 24 小时以上。如果业务允许,设 72 小时更安全(与 Broker 消息保留时间一致)。
Q:如果 Redis 和数据库方案结合使用,顺序是什么?
A:先 Redis 快速去重(减少 DB 压力),再 DB 兜底(保证最终一致)。流程:Redis setIfAbsent → 成功则执行业务 + 入库 → 失败则跳过。即使 Redis 丢失数据,DB 唯一索引也能兜底防重。
Q:RocketMQ 有没有内置的 Exactly Once 语义?
A:没有。RocketMQ 只保证 At Least Once。所谓的"Exactly Once"在分布式系统中几乎不可能完美实现,业界通用做法是 At Least Once + 业务幂等 来近似实现 Exactly Once 的效果。
5.5 业务落地
电商支付回调处理场景:第三方支付平台回调订单服务时,因网络抖动可能多次发送"支付成功"消息,若不做幂等,会导致订单重复更新状态、重复增加积分。解决方案:生产者(支付平台)将支付流水号作为 Keys;消费者(订单服务)消费前,用 Redis 的 setIfAbsent 判断该流水号是否已消费,未消费则执行更新逻辑并记录状态,同时在订单表的 pay_no 字段设置唯一索引,兜底重复消费。
6. 什么是幂等?如何解决幂等性问题
6.1 底层原理
幂等(Idempotent) 是数学概念,在编程中指:同样的请求发起一次和发起 N 次,系统的状态是一致的,不会产生副作用。
HTTP 方法幂等性:
| 方法 | 幂等 | 说明 |
|---|---|---|
| GET | ✅ | 查询操作,天然幂等 |
| PUT | ✅ | 全量更新,天然幂等 |
| DELETE | ✅ | 删除操作,天然幂等 |
| POST | ❌ | 新增操作,需手动实现幂等 |
| PATCH | ❌ | 部分更新,需手动实现幂等 |
6.2 幂等三大黄金方案
方案 1:数据库唯一约束(强一致最可靠)
try {
orderMapper.insert(order); // order 中包含唯一业务标识
} catch (DuplicateKeyException e) {
// 捕获唯一约束异常,视为已消费
log.info("重复消费,忽略: {}", order.getPayNo());
}
方案 2:Redis 分布式锁(高并发最常用)
String lockKey = "lock:order:" + orderId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(locked)) {
try {
processOrder(order);
} finally {
// 业务完成后可选择删除锁或等过期
}
} else {
log.info("正在处理或已处理,跳过: {}", orderId);
}
方案 3:状态机乐观锁(更新类最轻量)
-- 前置状态条件,只有状态匹配才更新
UPDATE orders SET status = 'PAID', update_time = NOW()
WHERE order_id = 'xxx' AND status = 'UNPAID';
-- 第二次执行:status 已是 'PAID',WHERE 条件不匹配,影响行数 = 0
6.3 注意事项
-
唯一标识需全局唯一(如 UUID、雪花 ID、业务流水号);
-
Redis 缓存需设置过期时间,避免缓存膨胀;
-
分布式场景下,需保证"校验-执行-记录"的原子性(如用 Lua 脚本或数据库事务);
-
幂等方案要分层设计:Redis 快速去重 → DB 唯一约束兜底 → 业务条件判断兜底。
6.4 追问
Q:幂等和去重有什么区别?
A:去重是手段,幂等是目标。去重是指"识别并跳过重复消息",幂等是指"即使重复执行,结果也一样"。去重不一定能完全覆盖所有情况(如 Redis 宕机),所以还需要业务层面的幂等设计来兜底。
Q:分布式事务场景下,如何保证幂等?
A:RocketMQ 事务消息的回查机制天然支持幂等——如果本地事务已经执行成功(状态已变更),回查时返回
COMMIT_MESSAGE,消息不会被重复投递。消费端仍需做幂等处理,因为消费者可能收到多次投递。
6.5 业务落地
用户积分发放场景(重复消费会导致资损):采用"唯一标识 + 缓存记录"方案,生产者(订单服务)发送消息时,将"用户 ID + 订单 ID"作为 Keys;消费者(积分服务)消费前,构建 Redis Key=mq:point:{userId}:{orderId},用 setIfAbsent 判断是否已消费;未消费则执行积分增加逻辑,记录消费状态(过期时间 24 小时);同时在积分明细表中,将"用户 ID + 订单 ID"设为唯一索引,兜底重复消费。
7. 如何处理消息队列消息积压问题
7.1 底层原理
消息积压的核心矛盾是:生产者的发送速度远大于消费者的处理速度。
积压根因排查清单:
| 原因 | 表现 | 排查方法 |
|---|---|---|
| 消费端 Bug | 消费进度停滞 | 检查消费端日志、死信队列 |
| 下游依赖超时 | 消费耗时突增 | 监控 DB/Redis/API 响应时间 |
| 流量突增 | 消息量暴增 | 对比生产/消费 TPS |
| 消费者宕机 | 消费者实例数减少 | 检查容器/K8s 状态 |
| Queue 数不足 | 扩容消费者无效 | 检查 Topic Queue 数量 |
7.2 处理方案(分两步)
第一步:紧急处理(治标)
10 倍扩容法(积压千万级时使用):
1. 临时新建 Topic,将 Queue 数量扩大 10~20 倍 2. 修改原消费者代码 → 改为"转发机器",只拉取积压消息,直接投递到新 Topic 3. 临时征用机器,部署 10 倍数量的消费者,消费新 Topic 消息 4. 快速清空积压后,恢复原架构
常规紧急操作:
-
扩容消费者实例(但不超过 Queue 数,超出的消费者分配不到 Queue);
-
调优消费参数:提高批量消费条数(
consumeMessageBatchMaxSize)、缩短拉取间隔; -
跳过非核心消息,优先消费核心消息;
-
如果消息已过期(如促销已结束),可直接清空积压。
第二步:根本解决(治本)
-
优化消费逻辑:排查并修复消费端 Bug,减少长事务、慢 SQL、慢 API 调用,将耗时操作异步化;
-
提升消费能力:将串行处理改为线程池异步处理,拆分消费链路,增加 Topic Queue 数量,提升并行度;
-
控制生产速度:高峰期对非核心业务降级,限制生产端发送速率,避免增量积压;
-
完善监控:监控积压量(消费延迟)、消费成功率,超过阈值触发预警,结合自动扩缩容机制。
7.3 关键公式
消费能力 = 消费者实例数 × 每实例消费线程数 × 单线程 TPS 积压清空时间 = 积压消息数 / (消费能力 - 生产速率)
注意:消费者实例数上限 = Topic 的 Read Queue 数量。如果消费者数超过 Queue 数,多余的消费者会闲置。所以扩容消费者前,先确认 Queue 数是否足够。
7.4 追问
Q:Queue 数量设多少合适?
A:Queue 数量决定了消费者的并行度上限。一般建议 Queue 数 = 消费者实例数 × 2~3,预留扩容空间。但 Queue 也不是越多越好——过多的 Queue 会增加 Broker 的文件句柄和内存开销,通常单 Topic 不超过 100 个 Queue。
Q:消费端用线程池并行消费,会不会影响顺序性?
A:会。如果使用了顺序消费(
MessageListenerOrderly),不能用线程池并行消费,否则会破坏顺序。并发消费(MessageListenerConcurrently)可以用线程池,但要确保业务不需要顺序保证。
Q:积压的消息已经过期了怎么办?
A:如果业务上已无意义(如促销已结束),可以直接跳过或清空。可以在消费逻辑中判断消息时间戳,超过有效期直接返回
CONSUME_SUCCESS(确认消费但不处理)。不要让过期消息继续占用消费资源。
7.5 业务落地
某电商大促期间,数据库连接池爆满,导致订单生成消息大量积压,处理步骤如下:
-
紧急处理:启动降级预案,用中转程序将几十万条积压消息快速搬运到具有 100 个 Queue 的新 Topic,部署一批临时容器并行消费落库,快速清空积压;
-
根本解决:排查代码,给下游数据库增加 Redis 缓存层,减少数据库查询压力;优化消费逻辑,将单条插入改为批量插入,提升消费速度;配置监控,积压量超过 10000 条触发短信预警,避免再次发生积压。
8. 消息队列技术选型:Kafka 还是 RocketMQ,还是 RabbitMQ
8.1 三大 MQ 全方位对比
| 维度 | RabbitMQ | RocketMQ | Kafka |
|---|---|---|---|
| 开发语言 | Erlang | Java | Scala/Java |
| 单机吞吐量 | 万级 QPS | 十万级 QPS | 百万级 QPS |
| 消息延迟 | 微秒级 | 毫秒级 | 毫秒级(批量优化后) |
| 消息可靠性 | 高(需手动持久化) | 极高(同步刷盘+复制) | 高(ISR 机制) |
| 顺序消息 | 不保证 | 原生支持 | Partition 内有序 |
| 事务消息 | 不支持 | 原生支持 | 支持(0.11+) |
| 延迟消息 | 插件支持 | 原生支持 18 级 | 不支持 |
| 死信队列 | 支持 | 原生支持 | 不支持 |
| 消息回溯 | 不支持 | 支持(按时间/offset) | 支持(按 offset) |
| 消息轨迹 | 不支持 | 原生支持 | 不支持 |
| 大数据生态 | 弱 | 弱 | 极强(Flink/Spark) |
| 运维复杂度 | 低 | 低 | 中等 |
| 社区活跃度 | 中 | 高(Apache 顶级) | 极高 |
8.2 选型决策树
你的业务场景是什么? ├── 大数据日志/流计算 → Kafka(极致吞吐 + 生态完善) ├── 电商/金融核心交易 → RocketMQ(事务消息 + 延迟消息 + 高可靠) ├── 中小系统/简单业务 → RabbitMQ(开箱即用 + 低延迟) └── 物联网/IoT → RocketMQ(支持 MQTT 协议扩展)
8.3 追问
Q:为什么 Kafka 吞吐量比 RocketMQ 高?
A:核心原因有三:① Kafka 使用顺序写磁盘 + 零拷贝(sendfile),单 Partition 写入是纯粹的顺序 I/O;② Kafka 采用批量发送(batch.size),多条消息打包一次网络请求;③ Kafka 的 Partition 是物理分区,每个 Partition 是独立的日志文件,并行度更高。RocketMQ 的 CommitLog 是所有 Topic 共享的单个文件,写入时需要加锁,吞吐量相对低一些。
Q:RabbitMQ 为什么不适合高并发场景?
A:RabbitMQ 基于 Erlang 的 Actor 模型,每条消息都要经过 Exchange 路由、Queue 存储、消息确认等复杂流程,且 Erlang 的 GC 机制在高并发下容易产生长停顿。另外,RabbitMQ 的 Queue 是内存+磁盘混合存储,消息堆积时性能下降明显。
Q:如果团队技术栈是 Go,选哪个 MQ?
A:RocketMQ 有 Go 客户端(
rocketmq-client-go),Kafka 有sarama和confluent-kafka-go。Go 团队做大数据选 Kafka,做业务选 RocketMQ。RabbitMQ 也有 Go 客户端(amqp-go-client),但 Erlang 运维成本高,不推荐。
8.4 业务落地
-
日志采集系统:选 Kafka,Flink 实时消费日志流,写入 Elasticsearch,吞吐量百万级无压力;
-
电商订单系统:选 RocketMQ,下单后异步通知库存、物流、积分服务,事务消息保证支付与订单状态一致;
-
内部管理系统:选 RabbitMQ,团队小、业务简单,开箱即用,维护成本低。
9. 消息中间件如何做到高可用
9.1 底层原理
分布式系统高可用的核心逻辑是:多副本冗余(Replication) + 故障自动转移(Failover),通过集群部署,避免单点故障,确保服务持续可用。
9.2 RocketMQ 高可用架构
NameServer 无状态集群
Producer ──→ NameServer-1 ──→ NameServer-2 ← 任意节点宕机不影响服务 ──→ NameServer-3
-
多个 NameServer 节点相互独立,不通信,任意一台宕机都不影响路由发现;
-
客户端可切换到其他节点获取路由信息,天然高可用。
Broker 多主多从集群
Broker-A (Master) ──同步复制──→ Broker-A (Slave) Broker-B (Master) ──同步复制──→ Broker-B (Slave) Producer ──→ Master-A / Master-B(自动切换) Consumer ──→ Master-A / Slave-A / Master-B / Slave-B
-
数据分片存储在多个 Master 节点,一个 Master 宕机,其他 Master 的数据不受影响;
-
生产者可自动切换到正常 Master 发送消息。
DLedger/Raft 自动主从切换(RocketMQ 4.5+)
Broker 组: ├── Candidate (竞选 Master) ├── Follower (同步数据) └── Follower (同步数据) Master 宕机 → Raft 选举 → Follower-A 成为新 Master → 自动切换
-
基于 Raft 一致性算法,当 Master 宕机时,集群会在秒级自动选举;
-
将数据最新的 Slave 提升为新 Master,实现无人工干预故障转移。
9.3 生产环境部署模式
| 模式 | 说明 | 可靠性 | 适用场景 |
|---|---|---|---|
| 单 Master | 单节点部署 | 低(宕机即丢) | 仅开发测试 |
| 多 Master | 多主无从 | 中(主宕机丢数据) | 非核心业务 |
| 多 Master 多 Slave(异步) | 主写从异步同步 | 高(可能丢少量) | 一般业务 |
| 多 Master 多 Slave(同步) | 主写从同步确认 | 极高(不丢数据) | 核心交易 |
| DLedger 模式 | Raft 自动选主 | 极高 + 自动故障转移 | 金融级 |
9.4 追问
Q:RocketMQ 的 Broker 主从模式和 Kafka 的 ISR 机制有什么区别?
A:Kafka 的 ISR(In-Sync Replicas)是动态副本集,只有 ISR 中的 Follower 才有资格被选为 Leader,Producer 可以配置
acks=all要求所有 ISR 确认。RocketMQ 的主从是静态配置,Master 宕机后需要人工切换(4.5 之前)或 DLedger 自动选举(4.5+)。Kafka 的 ISR 机制更灵活,但 RocketMQ 的 DLedger 模式已经追上了。
Q:NameServer 无状态,那 Broker 宕机后,Producer 怎么感知?
A:Producer 每 30 秒从 NameServer 更新路由表。如果 Broker 宕机,NameServer 在 120 秒后剔除该 Broker(心跳超时),Producer 下次更新路由时会发现 Broker 不可用,自动切换到其他 Broker。但在 120 秒窗口期内,发送到宕机 Broker 的消息会失败,触发重试机制(默认 3 次),重试时会选择其他 Broker。
9.5 业务落地
线上生产环境绝对不能单节点部署,标准架构至少是"双主双从(2m-2s)":核心交易系统强制开启主从同步复制(Sync Broker),确保消息同步到 Slave 后才返回成功;即使某台物理机突然断电、烧毁,依靠另一套主从节点,业务依然能正常运行,数据不丢失、服务不中断。
10. 如何保证数据一致性,事务消息如何实现
10.1 底层原理
微服务架构下,本地数据库事务无法覆盖 MQ 消息发送,容易产生分布式事务问题:
场景1:本地库操作成功(如扣钱),但 MQ 消息发送失败 → 下游收不到通知 场景2:MQ 消息发送成功,但本地库操作异常回滚 → 下游收到错误通知
核心需求是保证"本地操作"和"发消息"的最终一致性。
10.2 RocketMQ 事务消息实现(面试重点)
RocketMQ 原生支持事务消息(两阶段提交变种),通过"半消息 + 本地事务 + 事务回查"的机制,解决分布式一致性问题:
Producer Broker Consumer
│ │ │
│──1. 发送半消息(Half)──→ │ │
│ 对消费者不可见 │
│←──返回发送结果── │ │
│ │ │
│──2. 执行本地事务──→ MySQL │ │
│ (如:扣减余额) │ │
│ │ │
│──3. 提交/回滚──→ │ │
│ Commit → 消息可见 │──→ 投递给 Consumer ──→ │
│ Rollback → 删除消息 │ │
│ │ │
│ [如果3超时/丢失] │ │
│←──4. 事务回查── │ │
│ Broker 主动询问本地事务状态 │ │
│──返回 Commit/Rollback──→ │ │
详细步骤:
-
发送半消息(Half Message):生产者向 MQ 发送一条"半消息",消息到达 Broker 后,对消费者不可见,仅记录消息状态;
-
执行本地事务:生产者收到半消息发送成功的 ACK 后,执行本地业务逻辑(如写入 MySQL);
-
二次确认(Commit/Rollback):
-
本地事务执行成功 → 向 MQ 发送 Commit → 消息改为对外可见,消费者可拉取消费
-
本地事务执行失败 → 向 MQ 发送 Rollback → MQ 直接删除半消息
-
-
反查机制(兜底):若步骤 3 发生网络中断,MQ 迟迟未收到二次确认,会主动发起回查(默认 15 次,首次间隔 6s,后续 60s),调用生产者的
TransactionListener接口,查询本地数据库事务状态,再根据回查结果执行 Commit 或 Rollback。
10.3 代码实现
// 1. 定义事务监听器
public class OrderTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行本地事务(如更新订单状态)
orderService.updateOrderStatus(msg.getKeys(), "PAID");
return LocalTransactionState.COMMIT_MESSAGE; // 提交
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查本地事务状态
Order order = orderService.queryByOrderId(msg.getKeys());
if (order != null && "PAID".equals(order.getStatus())) {
return LocalTransactionState.COMMIT_MESSAGE; // 已完成,提交
}
return LocalTransactionState.UNKNOW; // 未完成,稍后再查
}
}
// 2. 发送事务消息
TransactionMQProducer producer = new TransactionMQProducer("tx_producer_group");
producer.setTransactionListener(new OrderTransactionListener());
producer.start();
Message msg = new Message("order_topic", "OrderID001".getBytes());
producer.sendMessageInTransaction(msg, null);
10.4 追问
Q:半消息存储在哪里?和普通消息一样吗?
A:半消息存储在 Broker 的内部 Topic
RMQ_SYS_TRANS_HALF_TOPIC中,与普通消息分开存储。当收到 Commit 后,Broker 会将半消息从内部 Topic 转移到目标 Topic(如order_topic),消费者才能看到。如果收到 Rollback,则直接标记删除。
Q:事务回查的次数和间隔是多少?能自定义吗?
A:默认回查 15 次,首次间隔 6 秒,后续每次间隔 60 秒。可以通过
transactionCheckMax和transactionCheckInterval配置。超过最大回查次数仍为 UNKNOW 状态,消息会被标记为回滚(删除)。
Q:RocketMQ 的事务消息和 Kafka 的事务有什么区别?
A:Kafka 的事务主要用于保证"跨 Partition 写入的原子性"(如同时写 Topic-A 和 Topic-B),本质是 Kafka 内部事务。RocketMQ 的事务消息解决的是"本地事务与 MQ 消息的一致性"(如数据库操作与发消息的原子性),本质是分布式事务方案。两者解决的问题不同,不能互相替代。
Q:如果回查时本地事务状态是 UNKNOW,会怎样?
A:Broker 会继续回查,直到超过最大回查次数(默认 15 次)。如果始终是 UNKNOW,消息最终会被回滚(删除)。所以本地事务的设计原则是:尽量能在短时间内确定状态,避免长时间处于中间状态。如果业务确实需要长时间处理(如 T+1 对账),建议将事务状态持久化到数据库,回查时读库判断。
10.5 业务落地
订单支付后发货通知场景:用户支付成功后,订单服务需要完成"更新订单状态(本地库操作)"和"向发货系统发送通知(MQ 消息)",两者必须保证一致。使用 RocketMQ 事务消息,在本地事务方法中执行 update order_status='PAID',通过事务回查监听器读取数据库该订单状态,确保本地操作和消息发送一致。这套机制在阿里交易链路中,经历了多次双 11 的高并发考验。
11. 让你写一个消息队列,该如何进行架构设计
11.1 底层原理
这是考察架构视野的系统设计题,设计任何中间件,都需从"网络通信、存储模型、高可用容灾、路由元数据管理"四个核心维度出发,兼顾性能、可靠性和可扩展性。
11.2 架构设计(参考 RocketMQ)
┌─────────────────────────────────────────────────────────────┐ │ 整体架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │Producer-1│ │Producer-2│ │Producer-N│ 生产者集群 │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └───────────────┼───────────────┘ │ │ │ │ │ ┌────────────────────▼────────────────────┐ │ │ │ NameServer (无状态集群) │ 路由注册中心 │ │ │ 保存 Broker 列表 + Topic → Queue 路由 │ │ │ └────────────────────┬────────────────────┘ │ │ │ │ │ ┌────────────────────▼────────────────────┐ │ │ │ Broker 集群 │ 消息存储核心 │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Master-A │ │ Master-B │ │ │ │ │ │ (CommitLog │ │ (CommitLog │ │ │ │ │ │ +CQueue+Idx)│ │ +CQueue+Idx)│ │ │ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ │ │ │ ┌──────▼──────┐ ┌──────▼──────┐ │ │ │ │ │ Slave-A │ │ Slave-B │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ └────────────────────┬────────────────────┘ │ │ │ │ │ ┌────────────────────▼────────────────────┐ │ │ │ Consumer 集群 │ 消费者集群 │ │ │ Pull 模式 + 长轮询 + 负载均衡 │ │ │ └─────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
11.3 各模块详细设计
1. 元数据管理(类似 NameServer)
-
设计轻量级注册中心,保存 Broker 节点列表和 Topic 路由信息;
-
采用无状态集群部署,各节点独立,不通信,保证高可用;
-
Broker 定时发送心跳,客户端定时拉取路由。
2. 网络通信层
-
使用 Netty 框架构建高性能 NIO 异步通信网络;
-
自定义报文协议(Header + Body),支持同步、异步发送;
-
连接池复用,减少 TCP 握手开销。
3. 存储引擎(核心重点)
写入流程: Producer → 追加写入 CommitLog(顺序 I/O,极快) → 同时写入 ConsumeQueue(索引,异步) → 同时写入 IndexFile(Key 索引,异步) 读取流程: Consumer → 查 ConsumeQueue 获取 CommitLog 偏移量 → 用 mmap/Zero-Copy 读取 CommitLog 消息体
-
不使用关系型数据库,采用磁盘顺序写模式,将所有消息追加到一个大文件(类似 CommitLog);
-
使用 mmap(内存映射) 实现零拷贝,减少 CPU 上下文切换和内存拷贝;
-
消息清理:定时删除过期文件(默认 72 小时),磁盘使用率超阈值触发清理。
4. 高可用集群
-
支持数据分片(Partition),实现横向扩容;
-
引入基于 Raft 协议的主从复制机制,保证数据强一致;
-
主节点宕机后自动选举新主,实现故障自动转移。
11.4 追问
Q:为什么不用 Kafka 那样的物理分区,而用 CommitLog 集中存储?
A:两者各有利弊。Kafka 的物理分区(每个 Partition 独立文件)写入并行度高,吞吐量更大,但文件数量多,运维复杂。RocketMQ 的 CommitLog 集中存储写入时需要加锁(内存锁),吞吐量略低,但文件管理简单,且支持消息回溯(因为所有消息在一个文件里)。对于业务消息场景(消息量没日志那么大),CommitLog 方案更合适。
Q:mmap 和 sendfile 的零拷贝有什么区别?
A:mmap 是将磁盘文件映射到用户空间虚拟内存,用户可以直接读写,省去了内核态到用户态的数据拷贝。sendfile 是直接在内核态完成文件到 Socket 的数据传输,完全不经过用户空间。RocketMQ 读消息用 mmap,Kafka 读消息用 sendfile。mmap 适合需要修改数据的场景,sendfile 适合纯转发场景。
11.5 业务落地
设计一个内部消息队列系统时,应优先考虑:① 存储引擎选用顺序写文件 + mmap,不引入外部数据库依赖;② 元数据管理用无状态节点,降低运维成本;③ 高可用基于 Raft 协议实现自动主从切换,减少人工干预。
12. 什么叫做阻塞队列的有界和无界
12.1 底层原理
在 Java 并发包(JUC)和 MQ 消费者内部缓冲设计中,常常用到阻塞队列(BlockingQueue),核心用于缓冲消息、协调生产者和消费者的速度,分为有界和无界两种:
| 类型 | 代表实现 | 容量 | 队列满时行为 |
|---|---|---|---|
| 有界队列 | ArrayBlockingQueue |
需指定 | 阻塞/抛异常/拒绝策略 |
| 无界队列 | LinkedBlockingQueue() |
~21亿 | 永远不会满 |
| 同步队列 | SynchronousQueue |
0 | 必须有消费者等待 |
12.2 关键避坑
在 MQ 消费端实现或任何高并发缓冲池设计中,绝对禁止使用无界队列!
生产者持续发送 → 无界队列不断堆积 → JVM 堆内存耗尽 → OOM 崩溃
-
一旦消费者因数据库卡顿、处理缓慢,而生产端仍在持续发送消息,无界队列会不断堆积消息,导致 JVM 内存耗尽,引发 OOM(Out Of Memory)崩溃;
-
必须使用有界队列,并配置合理的降级和拒绝策略(如
CallerRunsPolicy),避免内存溢出。
12.3 Java 线程池拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) |
抛出 RejectedExecutionException | 需要感知积压 |
CallerRunsPolicy |
由提交任务的线程执行 | 不丢消息,降速 |
DiscardPolicy |
静默丢弃 | 可丢消息的场景 |
DiscardOldestPolicy |
丢弃队列最旧的任务 | 只关心最新数据 |
12.4 追问
Q:RocketMQ 消费者的内部缓冲队列是有界还是无界?
A:RocketMQ 的
DefaultMQPushConsumer内部使用的是有界队列(ProcessQueue中的TreeMap),通过pullThresholdForQueue(默认 1000 条)控制单个 Queue 的缓存上限,超过阈值会暂停拉取,避免内存溢出。
Q:Kafka 消费者的缓冲队列呢?
A:Kafka 消费者通过
fetch.max.bytes和max.partition.fetch.bytes控制单次拉取的数据量上限,本质也是有界控制。消费者维护的是一个拉取缓冲区,不是传统的阻塞队列。
12.5 业务落地
MQ 消费端的线程池设计:使用 ArrayBlockingQueue(1000) 作为有界缓冲队列,配合 CallerRunsPolicy 拒绝策略——当队列满时,由调用线程(拉取线程)直接执行消费任务,自然降低拉取速度,形成背压机制,保护系统不被压垮。
额外重点:RocketMQ 为什么要放弃 ZooKeeper
20.1 底层原理
CAP 理论指出,一致性(C)、可用性(A)、分区容错性(P)三者不可兼得:
| 模型 | 优先保证 | 牺牲 | 代表 |
|---|---|---|---|
| CP | 一致性 | 可用性 | ZooKeeper、Etcd |
| AP | 可用性 | 强一致性 | NameServer、Eureka |
20.2 RocketMQ 放弃 ZooKeeper 原因
早期 RocketMQ 借鉴 Kafka,采用 ZooKeeper 作为注册中心,后来阿里自研 NameServer 将其替换,核心原因有两点:
-
CAP 理论取舍:ZooKeeper 是 CP 模型,发生网络分区或主节点选举时,会短暂无法提供服务(不可用);而 MQ 路由发现,允许客户端拿到几秒前的旧路由(大不了发送失败重试),但绝不允许注册中心宕机拒绝服务,因此更需要高可用的 AP 模型,NameServer 恰好满足这一需求。
-
架构极简:ZooKeeper 过于沉重,需维护独立集群,运维成本高;NameServer 设计极其轻量,各节点相互独立、无状态,部署和维护简单,同时保证了极高的可用性,符合 MQ 路由服务的核心需求。
20.3 追问
Q:Kafka 也在去 ZooKeeper 化(KIP-500),说明什么?
A:Kafka 从 3.0 开始支持 KRaft 模式(基于 Raft 的内置元数据管理),去掉了对 ZooKeeper 的依赖。这说明业界普遍认为 ZooKeeper 作为 MQ 的元数据管理过于"重"了。Kafka 的 KRaft 模式和 RocketMQ 的 NameServer 思路类似——都是自研轻量级方案替代外部依赖。
Q:NameServer 无状态,那 Broker 注册信息重启后会丢吗?
A:会。NameServer 重启后内存中的路由信息全部丢失,但 Broker 每 30 秒会重新发送心跳注册,Producer/Consumer 每 30 秒重新拉取路由,所以最多 30 秒后路由信息就会恢复。这个窗口期内,发送/消费会短暂失败,但会自动重试,不会丢消息。
补充高频追问
Q1:RocketMQ 的消息模型是什么?
RocketMQ 使用发布/订阅模型,核心概念:
-
Topic:消息的逻辑分类(如
order_topic) -
Tag:消息的二级标签(如
order_topic:pay、order_topic:refund),用于消费者过滤 -
Queue:Topic 的物理分片(类似 Kafka 的 Partition),实现并行消费
-
ConsumerGroup:消费者组,同一组内的消费者共同消费一个 Topic 的消息(集群模式)
Q2:RocketMQ 的 Pull 和 Push 模式有什么区别?
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| Pull | 消费者主动拉取 | 消费者控制速率 | 实时性差,需轮询 |
| Push | Broker 推送(基于 Pull 封装) | 实时性好 | 消费者无法控制速率 |
RocketMQ 的 Push 模式底层其实是长轮询(Long Polling):消费者发起拉取请求,如果没有新消息,Broker 会 hold 住连接(默认 15 秒),有新消息时立即返回,模拟"推送"效果。
Q3:RocketMQ 的消息积压监控怎么做?
-
消费延迟:
ConsumerOffset与MaxOffset的差值,差值越大积压越严重 -
消费 TPS:单位时间消费消息条数,突然下降说明消费端有问题
-
死信队列:监控
%DLQ%Topic 中的消息数量,有消息说明消费多次失败 -
管理控制台:RocketMQ Dashboard 可视化展示所有指标
Q4:RocketMQ 的 Tag 过滤是在 Broker 端还是 Consumer 端?
Broker 端。Consumer 订阅时将 Tag 表达式发送给 Broker,Broker 在 ConsumeQueue 中通过 Tag 的 HashCode 进行预过滤,只有匹配的消息才会被拉取到 Consumer,减少无效数据传输。
Q5:RocketMQ 支持哪些消息类型?
| 消息类型 | 说明 | 典型场景 |
|---|---|---|
| 普通消息 | 同步/异步/单向发送 | 通用场景 |
| 顺序消息 | 同一 Queue 内有序 | Binlog 同步 |
| 延迟消息 | 指定延迟等级投递 | 定时任务、超时取消 |
| 事务消息 | 半消息 + 本地事务 | 分布式事务 |
| 批量消息 | 多条消息打包发送 | 高吞吐场景 |
| 过滤消息 | Tag/SQL 表达式过滤 | 多消费者按需消费 |
Q6:RocketMQ 的延迟消息原理?
Producer → 发送延迟消息 → Broker 存入内部 Topic (SCHEDULE_TOPIC_XXXX) ↓ 定时任务扫描到期消息 ↓ 投递到目标 Topic → Consumer 消费
-
延迟消息暂存在 Broker 的内部 Topic
SCHEDULE_TOPIC_XXXX中; -
Broker 定时任务每秒扫描一次到期消息,投递到目标 Topic;
-
开源版支持 18 级固定延迟(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h),商业版支持自定义时间。
Q7:RocketMQ 的死信队列是什么?
当消息消费失败次数超过最大重试次数(默认 16 次)后,消息不会被丢弃,而是进入死信队列(Dead Letter Queue,DLQ),Topic 名称为 %DLQ%{ConsumerGroup}。死信队列中的消息需要人工介入处理——可以手动消费、修复后重发,或直接丢弃。
总结
MQ 技术的深浅,必须抓住其底层核心逻辑:
-
存储模型:磁盘顺序读写(CommitLog) + 零拷贝(mmap/sendfile)
-
高可用模型:Raft 选举 + 多副本复制 + 无状态注册中心
-
分布式环境应对:ACK 确认 + 重试机制 + 半事务消息 + 业务幂等
-
性能优化:批量发送 + 异步化 + 长轮询 + 索引分离
面试时,按"底层原理 → RocketMQ 实现 → 对比 Kafka/RabbitMQ → 业务落地"的结构回答,既展示深度,又展示广度。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)